diff --git a/.buildkite/commands/lint.sh b/.buildkite/commands/lint.sh
new file mode 100755
index 00000000000..ccb16770559
--- /dev/null
+++ b/.buildkite/commands/lint.sh
@@ -0,0 +1,18 @@
+#!/bin/bash -u
+
+echo "--- 🧹 Linting"
+cp gradle.properties-example gradle.properties
+./gradlew :WooCommerce:lintJalapenoDebug
+app_lint_exit_code=$?
+
+./gradlew :WooCommerce-Wear:lintJalapenoDebug
+wear_lint_exit_code=$?
+
+lint_exit_code=0
+if [ $app_lint_exit_code -ne 0 ] || [ $wear_lint_exit_code -ne 0 ]; then
+ lint_exit_code=1
+fi
+
+upload_sarif_to_github 'WooCommerce/build/reports/lint-results-jalapenoDebug.sarif'
+
+exit $lint_exit_code
diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml
index 2a015a8a3bf..a6cd852fc8d 100644
--- a/.buildkite/pipeline.yml
+++ b/.buildkite/pipeline.yml
@@ -44,10 +44,7 @@ steps:
- "**/build/reports/detekt/detekt.html"
- label: "lint"
- command: |
- echo "--- 🧹 Linting"
- cp gradle.properties-example gradle.properties
- ./gradlew lintJalapenoDebug
+ command: .buildkite/commands/lint.sh
plugins: [$CI_TOOLKIT]
artifact_paths:
- "**/build/reports/lint-results*.*"
diff --git a/.buildkite/release-builds.yml b/.buildkite/release-builds.yml
index e0f72e0bbe1..a81fa31e342 100644
--- a/.buildkite/release-builds.yml
+++ b/.buildkite/release-builds.yml
@@ -24,6 +24,10 @@ steps:
plugins: [$CI_TOOLKIT]
notify:
- slack: "#build-and-ship"
+ retry:
+ manual:
+ # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite
+ allowed: false
- label: "🛠 Release Build (Wear App)"
command: |
diff --git a/.buildkite/release-pipelines/new-hotfix-release.yml b/.buildkite/release-pipelines/new-hotfix-release.yml
index 2209b01bfef..e284d891f06 100644
--- a/.buildkite/release-pipelines/new-hotfix-release.yml
+++ b/.buildkite/release-pipelines/new-hotfix-release.yml
@@ -2,7 +2,7 @@
---
steps:
- - input: "What version code do you want to use for the new hotfix release?"
+ - block: "What version code do you want to use for the new hotfix release?"
fields:
- text: "Version Code"
key: "version_code"
@@ -16,11 +16,12 @@ steps:
echo '--- :ruby: Setup Ruby Tools'
install_gems
- # Get the version code from the Buildkite 'input' step
+ # Get the version code from the Buildkite 'block' step
VERSION_CODE=$(buildkite-agent meta-data get version_code)
echo '--- :fire: Start New Hotfix Release'
- bundle exec fastlane new_hotfix_release version_name:${VERSION} version_code:${VERSION_CODE} skip_confirm:true
+ # Note: we need to double the dollar sign in front of `{VERSION_CODE}` below, so that the env var is interpreted *at runtime*, instead of too soon during `pipeline upload`
+ bundle exec fastlane new_hotfix_release version_name:$${VERSION} version_code:$${VERSION_CODE} skip_confirm:true
agents:
queue: "tumblr-metal"
retry:
diff --git a/.buildkite/release-pipelines/publish-release.yml b/.buildkite/release-pipelines/publish-release.yml
new file mode 100644
index 00000000000..2937ccb0cca
--- /dev/null
+++ b/.buildkite/release-pipelines/publish-release.yml
@@ -0,0 +1,22 @@
+steps:
+ - label: "Publish Release"
+ plugins:
+ - $CI_TOOLKIT
+ command: |
+ echo '--- :robot_face: Use bot for git operations'
+ source use-bot-for-git
+
+ echo '--- :git: Checkout Release Branch'
+ .buildkite/commands/checkout-release-branch.sh
+
+ echo '--- :ruby: Setup Ruby Tools'
+ install_gems
+
+ echo '--- :package: Publish Release'
+ bundle exec fastlane publish_release skip_confirm:true include_wear_app:"${INCLUDE_WEAR_APP:-false}"
+ agents:
+ queue: "tumblr-metal"
+ retry:
+ manual:
+ # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite
+ allowed: false
diff --git a/.buildkite/shared-pipeline-vars b/.buildkite/shared-pipeline-vars
index 279aa805e58..307dd62e628 100644
--- a/.buildkite/shared-pipeline-vars
+++ b/.buildkite/shared-pipeline-vars
@@ -1,7 +1,8 @@
#!/bin/sh
- # This file is `source`'d before calling `buildkite-agent pipeline upload`, and can be used
- # to set up some variables that will be interpolated in the `.yml` pipeline before uploading it.
+# This file is `source`'d before calling `buildkite-agent pipeline upload`, and can be used
+# to set up some variables that will be interpolated in the `.yml` pipeline before uploading it.
+
+export CI_TOOLKIT="automattic/a8c-ci-toolkit#3.7.1"
+export TEST_COLLECTOR="test-collector#v1.10.1"
- export CI_TOOLKIT="automattic/a8c-ci-toolkit#3.5.1"
- export TEST_COLLECTOR="test-collector#v1.10.1"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44d7134bad0..c873d143b68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,18 @@
+## 20.5
+We’re excited to bring you a smoother experience with our latest update! We've resolved an issue that prevented renaming Product Variation Attributes and fixed a bug related to notification removal. Plus, users can now easily select product images when creating Blaze ads, and the Blaze feature is now fully enabled for sites with the Blaze for WooCommerce plugin active. Enjoy the improvements!
+
+## 20.4
+We've enhanced your WooCommerce app experience! You can now effortlessly scan tracking numbers when adding them to orders. Plus, we've fixed an issue with shipping labels, ensuring accurate weight calculations for packages with multiple items. Enjoy a smoother, more reliable app experience!
+
+## 20.3
+We’ve made several enhancements to boost your experience! This update fixes crashes on the orders list screen, resolves issues with the Jetpack plugin installation, and corrects toolbar glitches in the Package details and Order Tracking screens. We’ve also added a helpful blaze campaign reminder if you leave a campaign creation unfinished. Enjoy smoother navigation and a more reliable app!
+
+## 20.2
+Create Blaze Evergreen campaigns effortlessly with our latest update! We've also improved tablet navigation—now you'll be redirected to the order details screen after payment for a smoother experience. Update today and enjoy the enhancements!
+
## 20.1
We've improved your experience with our latest update! We've resolved the issue where the Orders screen would get stuck, addressed incorrect pin errors for UK and Canadian stores, fixed a rare crash in order editing, and enhanced usability on small screens. Enjoy smoother performance and better reliability!
diff --git a/Dangerfile b/Dangerfile
index e7eb896fbf1..54f6cee2166 100644
--- a/Dangerfile
+++ b/Dangerfile
@@ -3,7 +3,7 @@
github.dismiss_out_of_range_messages
# `files: []` forces rubocop to scan all files, not just the ones modified in the PR
-# Prevent RuboCop from running using `bundle exec`, which we don't want on the linter agent
+# `skip_bundle_exec` prevents RuboCop from running using `bundle exec`, which we don't want on the linter agent
rubocop.lint(files: [], force_exclusion: true, inline_comment: true, fail_on_inline_comment: true, include_cop_names: true, skip_bundle_exec: true)
manifest_pr_checker.check_gemfile_lock_updated
@@ -54,4 +54,10 @@ labels_checker.check(
)
# runs the milestone check if this is not a WIP feature and the PR is against the main branch or the release branch
-milestone_checker.check_milestone_due_date(days_before_due: 2) if (github_utils.main_branch? || github_utils.release_branch?) && !github_utils.wip_feature?
+if (github_utils.main_branch? || github_utils.release_branch?) && !github_utils.wip_feature?
+ report_type = github.pr_labels.include?('milestone-not-required') || github.pr_labels.include?('status: feature-flagged') ? :message : :error
+ milestone_checker.check_milestone_due_date(
+ days_before_due: 2,
+ report_type_if_no_milestone: report_type
+ )
+end
diff --git a/Gemfile b/Gemfile
index 91634fcd7a0..1714d277893 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,11 +5,11 @@ source 'https://rubygems.org'
gem 'danger-dangermattic', '~> 1.1'
gem 'fastlane', '~> 2.216'
gem 'nokogiri'
-gem 'rubocop', '~> 1.60'
+gem 'rubocop', '~> 1.65'
### Fastlane Plugins
-gem 'fastlane-plugin-wpmreleasetoolkit', '~> 11.0'
+gem 'fastlane-plugin-wpmreleasetoolkit', '~> 12.0'
# gem 'fastlane-plugin-wpmreleasetoolkit', path: '../../release-toolkit'
# gem 'fastlane-plugin-wpmreleasetoolkit', git: 'https://github.com/wordpress-mobile/release-toolkit', branch: ''
diff --git a/Gemfile.lock b/Gemfile.lock
index f31f3a70516..00847ca92f0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -5,16 +5,17 @@ GEM
base64
nkf
rexml
- activesupport (7.1.3.4)
+ activesupport (7.2.1)
base64
bigdecimal
- concurrent-ruby (~> 1.0, >= 1.0.2)
+ concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
+ logger (>= 1.4.2)
minitest (>= 5.1)
- mutex_m
- tzinfo (~> 2.0)
+ securerandom (>= 0.3)
+ tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
@@ -51,7 +52,7 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
- concurrent-ruby (1.3.3)
+ concurrent-ruby (1.3.4)
connection_pool (2.4.1)
cork (0.3.0)
colored2 (~> 3.1)
@@ -158,7 +159,7 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
- fastlane-plugin-wpmreleasetoolkit (11.1.0)
+ fastlane-plugin-wpmreleasetoolkit (12.0.0)
activesupport (>= 6.1.7.1)
buildkit (~> 1.5)
chroma (= 0.2.0)
@@ -167,7 +168,7 @@ GEM
git (~> 1.3)
google-cloud-storage (~> 1.31)
java-properties (~> 0.3.0)
- nokogiri (~> 1.11, < 1.17)
+ nokogiri (~> 1.11)
octokit (~> 6.1)
parallel (~> 1.14)
plist (~> 3.1)
@@ -231,13 +232,13 @@ GEM
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
language_server-protocol (3.17.0.3)
+ logger (1.6.1)
mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
- minitest (5.24.1)
+ minitest (5.25.1)
multi_json (1.15.0)
multipart-post (2.4.1)
- mutex_m (0.2.0)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1)
@@ -252,8 +253,8 @@ GEM
options (2.3.2)
optparse (0.5.0)
os (1.1.4)
- parallel (1.25.1)
- parser (3.3.4.1)
+ parallel (1.26.3)
+ parser (3.3.4.2)
ast (~> 2.4.1)
racc
plist (3.7.1)
@@ -273,7 +274,7 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
- rexml (3.3.4)
+ rexml (3.3.6)
strscan
rmagick (4.3.0)
rouge (2.0.7)
@@ -288,7 +289,7 @@ GEM
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
- rubocop-ast (1.32.0)
+ rubocop-ast (1.32.1)
parser (>= 3.3.1.0)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
@@ -296,6 +297,7 @@ GEM
sawyer (0.9.2)
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
+ securerandom (0.3.1)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
@@ -337,10 +339,10 @@ PLATFORMS
DEPENDENCIES
danger-dangermattic (~> 1.1)
fastlane (~> 2.216)
- fastlane-plugin-wpmreleasetoolkit (~> 11.0)
+ fastlane-plugin-wpmreleasetoolkit (~> 12.0)
nokogiri
rmagick (~> 4.1)
- rubocop (~> 1.60)
+ rubocop (~> 1.65)
BUNDLED WITH
2.4.19
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 9c5d6eda306..9beffdf661a 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -2,11 +2,43 @@
*** Use [*****] to indicate smoke tests of all critical flows should be run on the final APK before release (e.g. major library or targetSdk updates).
*** For entries which are touching the Android Wear app's, start entry with `[WEAR]` too.
+20.6
+-----
+- [*] Users can set a password to protect products when signed in using site credentials (compatible with WooCommerce 8.1.0 and higher) [https://github.com/woocommerce/woocommerce-android/pull/12642]
+
+20.5
+-----
+- [*] Fixes a bug that prevented users to rename the Product Variation Attributes to because of case insensitive checks [https://github.com/woocommerce/woocommerce-android/pull/12608]
+- [*] Users can directly pick product images when creating Blaze ads [https://github.com/woocommerce/woocommerce-android/pull/12610]
+- [*] Enables Blaze feature for sites with Blaze for WooCommerce plugin installed and active [https://github.com/woocommerce/woocommerce-android/pull/12640]
+- [*] Fix for ConcurrentModificationException while removing notification [https://github.com/woocommerce/woocommerce-android/pull/12646]
+
+20.4
+-----
+- [**] Users can now scan their tracking number when adding it to the order [https://github.com/woocommerce/woocommerce-android/pull/12533]
+- [*] Fixed an issue where shipping labels were incorrectly calculating the weight for packages containing multiple quantities of the same product [https://github.com/woocommerce/woocommerce-android/pull/12602]
+- [**] [WEAR] Introduces three new complications for the Wear app: Orders Total, Orders Count, and Visitors [https://github.com/woocommerce/woocommerce-android/pull/12558]
+
+20.3
+-----
+- [**] Stopped potential crash in the orders list screen when tapping "Filters" button [https://github.com/woocommerce/woocommerce-android/pull/12482]
+- [**] Fixed a bug that caused the app to freeze after adding products to new order [https://github.com/woocommerce/woocommerce-android/pull/12493]
+- [*] Fixes error when installing Jetpack plugin from the app [https://github.com/woocommerce/woocommerce-android/issues/12364]
+- [*] [Internal] Updated the Compose WebView component to handle choosing files from the device when the web page requests it [https://github.com/woocommerce/woocommerce-android/pull/12499]
+- [*] Fixes toolbar shown twice on Package details screen while adding shipping label [https://github.com/woocommerce/woocommerce-android/pull/12489]
+- [*] Fixes navigation from product review detail screen when opened from notification [https://github.com/woocommerce/woocommerce-android/pull/12514]
+- [*] Added a blaze campaign reminder that appears after abandoning a campaign creation. [https://github.com/woocommerce/woocommerce-android/pull/12452]
+- [**] [Internal] Fix a crash during JITM cache initialisation
+- [*] Fixes an issue that caused the an incorrect App Bar visibility in some rare cases [https://github.com/woocommerce/woocommerce-android/pull/12523]
+- [*] Fixes duplicated toolbar in the Order Tracking Carriers List [https://github.com/woocommerce/woocommerce-android/pull/12537]
+- [*] Fixes an issue on the Order Tracking screen that was causing the carrier being reset unintentionally [https://github.com/woocommerce/woocommerce-android/pull/12535]
+
20.2
-----
- [**] Fixed navigation in tablet mode: after order is paid, the app should redirect to order details screen. [https://github.com/woocommerce/woocommerce-android/pull/12415]
- [*] Show the back button on the Order Edit Screen for phones [https://github.com/woocommerce/woocommerce-android/pull/12421]
-
+- [**] Enables support to create Blaze Evergreen campaigns [https://github.com/woocommerce/woocommerce-android/issues/12176]
+- [*] Fixed a bug that caused the button "Open Mail" to not work during Magic Link login [https://github.com/woocommerce/woocommerce-android/pull/12486]
20.1
-----
@@ -16,6 +48,7 @@
- [*] [Internal] Fixed an issue with date formatting for the Blaze campaign creation API request [https://github.com/woocommerce/woocommerce-android/pull/12372]
- [*] Fixed a rare crash in the order editing screen. [https://github.com/woocommerce/woocommerce-android/pull/12382]
- [*] Fixed the blaze budget screens that are unusable on small screens. [https://github.com/woocommerce/woocommerce-android/pull/12402]
+- [**] Fixed navigation on "Collect payment" button tap in order creation flow [https://github.com/woocommerce/woocommerce-android/pull/12436]
20.0
-----
diff --git a/WooCommerce-Wear/build.gradle b/WooCommerce-Wear/build.gradle
index 29b89cc7dcb..9af1466b7fb 100644
--- a/WooCommerce-Wear/build.gradle
+++ b/WooCommerce-Wear/build.gradle
@@ -215,6 +215,7 @@ dependencies {
// See https://github.com/wordpress-mobile/WordPress-FluxC-Android/issues/919
exclude group: 'com.squareup.okhttp3'
}
+ lintChecks "com.android.security.lint:lint:$securityLintVersion"
}
def checkGradlePropertiesFile() {
diff --git a/WooCommerce-Wear/src/main/AndroidManifest.xml b/WooCommerce-Wear/src/main/AndroidManifest.xml
index 555eae15a1b..4cb65b7eaf6 100644
--- a/WooCommerce-Wear/src/main/AndroidManifest.xml
+++ b/WooCommerce-Wear/src/main/AndroidManifest.xml
@@ -31,9 +31,63 @@
android:value="@integer/google_play_services_version" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -96,6 +150,7 @@
android:name=".app.WearMainActivity"
android:exported="true"
android:taskAffinity=""
+ android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
android:theme="@style/MainActivityTheme.Starting">
diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/ComplicationUtils.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/ComplicationUtils.kt
index 31852d3848c..4477e6787bf 100644
--- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/ComplicationUtils.kt
+++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/ComplicationUtils.kt
@@ -35,7 +35,7 @@ private fun createAppPendingIntent(context: Context): PendingIntent {
}
private fun createIcon(context: Context) = MonochromaticImage.Builder(
- Icon.createWithResource(context, R.drawable.img_woo_bubble_white),
+ Icon.createWithResource(context, R.drawable.img_woo_bubble_monochrome),
).build()
private fun String.toComplicationText() = PlainComplicationText.Builder(this).build()
diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/FetchStatsForComplications.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/FetchStatsForComplications.kt
new file mode 100644
index 00000000000..09aff59e893
--- /dev/null
+++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/FetchStatsForComplications.kt
@@ -0,0 +1,69 @@
+package com.woocommerce.android.wear.complications
+
+import android.icu.text.CompactDecimalFormat
+import com.woocommerce.android.wear.complications.FetchStatsForComplications.StatType.ORDER_COUNT
+import com.woocommerce.android.wear.complications.FetchStatsForComplications.StatType.ORDER_TOTALS
+import com.woocommerce.android.wear.complications.FetchStatsForComplications.StatType.VISITORS_COUNT
+import com.woocommerce.android.wear.ui.login.LoginRepository
+import com.woocommerce.android.wear.ui.stats.datasource.StatsRepository
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.firstOrNull
+import org.wordpress.android.fluxc.model.SiteModel
+import javax.inject.Inject
+
+class FetchStatsForComplications @Inject constructor(
+ private val coroutineScope: CoroutineScope,
+ private val statsRepository: StatsRepository,
+ private val loginRepository: LoginRepository,
+ private val decimalFormat: CompactDecimalFormat
+) {
+ suspend operator fun invoke(statType: StatType): String {
+ val site = coroutineScope.async {
+ loginRepository.selectedSiteFlow
+ .filterNotNull()
+ .firstOrNull()
+ }.await() ?: return DEFAULT_EMPTY_VALUE
+
+ return when (statType) {
+ ORDER_TOTALS -> fetchTodayOrderTotals(site)
+ ORDER_COUNT -> fetchTodayOrderCount(site)
+ VISITORS_COUNT -> fetchTodayVisitors(site)
+ }
+ }
+
+ private suspend fun fetchTodayOrderTotals(
+ site: SiteModel
+ ) = site.let { statsRepository.fetchRevenueStats(it) }
+ .getOrNull()
+ ?.parseTotal()
+ ?.totalSales
+ ?.let { decimalFormat.format(it) }
+ ?: DEFAULT_EMPTY_VALUE
+
+ private suspend fun fetchTodayOrderCount(
+ site: SiteModel
+ ) = site.let { statsRepository.fetchRevenueStats(it) }
+ .getOrNull()
+ ?.parseTotal()
+ ?.ordersCount?.toString()
+ ?: DEFAULT_EMPTY_VALUE
+
+ private suspend fun fetchTodayVisitors(
+ site: SiteModel
+ ) = site.let { statsRepository.fetchVisitorStats(it) }
+ .getOrNull()
+ ?.toString()
+ ?: DEFAULT_EMPTY_VALUE
+
+ enum class StatType {
+ ORDER_TOTALS,
+ ORDER_COUNT,
+ VISITORS_COUNT
+ }
+
+ companion object {
+ const val DEFAULT_EMPTY_VALUE = "N/A"
+ }
+}
diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/ordercount/OrderCountComplicationService.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/ordercount/OrderCountComplicationService.kt
new file mode 100644
index 00000000000..aa4d1bc498f
--- /dev/null
+++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/ordercount/OrderCountComplicationService.kt
@@ -0,0 +1,42 @@
+package com.woocommerce.android.wear.complications.ordercount
+
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.datasource.ComplicationRequest
+import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService
+import com.woocommerce.android.R
+import com.woocommerce.android.wear.complications.FetchStatsForComplications
+import com.woocommerce.android.wear.complications.FetchStatsForComplications.StatType.ORDER_COUNT
+import com.woocommerce.android.wear.complications.createTextComplicationData
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class OrderCountComplicationService : SuspendingComplicationDataSourceService() {
+
+ @Inject lateinit var fetchStatsForComplications: FetchStatsForComplications
+
+ override fun getPreviewData(type: ComplicationType): ComplicationData? {
+ return when (type) {
+ ComplicationType.SHORT_TEXT -> createTextComplicationData(
+ context = applicationContext,
+ content = getString(R.string.order_count_complication_preview_value),
+ description = getString(R.string.order_count_complication_preview_description)
+ )
+
+ else -> null
+ }
+ }
+
+ override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? {
+ return when (request.complicationType) {
+ ComplicationType.SHORT_TEXT -> createTextComplicationData(
+ context = applicationContext,
+ content = fetchStatsForComplications(ORDER_COUNT),
+ description = getString(R.string.order_count_complication_preview_description)
+ )
+
+ else -> null
+ }
+ }
+}
diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/ordertotals/OrderTotalsComplicationService.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/ordertotals/OrderTotalsComplicationService.kt
new file mode 100644
index 00000000000..5e485dfe61c
--- /dev/null
+++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/ordertotals/OrderTotalsComplicationService.kt
@@ -0,0 +1,42 @@
+package com.woocommerce.android.wear.complications.ordertotals
+
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.datasource.ComplicationRequest
+import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService
+import com.woocommerce.android.R
+import com.woocommerce.android.wear.complications.FetchStatsForComplications
+import com.woocommerce.android.wear.complications.FetchStatsForComplications.StatType.ORDER_TOTALS
+import com.woocommerce.android.wear.complications.createTextComplicationData
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class OrderTotalsComplicationService : SuspendingComplicationDataSourceService() {
+
+ @Inject lateinit var fetchStatsForComplications: FetchStatsForComplications
+
+ override fun getPreviewData(type: ComplicationType): ComplicationData? {
+ return when (type) {
+ ComplicationType.SHORT_TEXT -> createTextComplicationData(
+ context = applicationContext,
+ content = getString(R.string.order_totals_complication_preview_value),
+ description = getString(R.string.order_totals_complication_preview_description)
+ )
+
+ else -> null
+ }
+ }
+
+ override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? {
+ return when (request.complicationType) {
+ ComplicationType.SHORT_TEXT -> createTextComplicationData(
+ context = applicationContext,
+ content = fetchStatsForComplications(ORDER_TOTALS),
+ description = getString(R.string.order_totals_complication_preview_description)
+ )
+
+ else -> null
+ }
+ }
+}
diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/AppShortcutComplicationService.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/shortcut/AppShortcutComplicationService.kt
similarity index 90%
rename from WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/AppShortcutComplicationService.kt
rename to WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/shortcut/AppShortcutComplicationService.kt
index 958f477a174..602b48d89be 100644
--- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/AppShortcutComplicationService.kt
+++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/shortcut/AppShortcutComplicationService.kt
@@ -1,10 +1,11 @@
-package com.woocommerce.android.wear.complications
+package com.woocommerce.android.wear.complications.shortcut
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.datasource.ComplicationRequest
import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService
import com.woocommerce.android.R
+import com.woocommerce.android.wear.complications.createImageComplicationData
class AppShortcutComplicationService : SuspendingComplicationDataSourceService() {
override fun getPreviewData(type: ComplicationType): ComplicationData? {
diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/visitorscount/VisitorsCountComplicationService.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/visitorscount/VisitorsCountComplicationService.kt
new file mode 100644
index 00000000000..18d64f521df
--- /dev/null
+++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/complications/visitorscount/VisitorsCountComplicationService.kt
@@ -0,0 +1,42 @@
+package com.woocommerce.android.wear.complications.visitorscount
+
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.datasource.ComplicationRequest
+import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService
+import com.woocommerce.android.R
+import com.woocommerce.android.wear.complications.FetchStatsForComplications
+import com.woocommerce.android.wear.complications.FetchStatsForComplications.StatType.VISITORS_COUNT
+import com.woocommerce.android.wear.complications.createTextComplicationData
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class VisitorsCountComplicationService : SuspendingComplicationDataSourceService() {
+
+ @Inject lateinit var fetchStatsForComplications: FetchStatsForComplications
+
+ override fun getPreviewData(type: ComplicationType): ComplicationData? {
+ return when (type) {
+ ComplicationType.SHORT_TEXT -> createTextComplicationData(
+ context = applicationContext,
+ content = getString(R.string.visitors_count_complication_preview_value),
+ description = getString(R.string.visitors_count_complication_preview_description)
+ )
+
+ else -> null
+ }
+ }
+
+ override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? {
+ return when (request.complicationType) {
+ ComplicationType.SHORT_TEXT -> createTextComplicationData(
+ context = applicationContext,
+ content = fetchStatsForComplications(VISITORS_COUNT),
+ description = getString(R.string.visitors_count_complication_preview_description)
+ )
+
+ else -> null
+ }
+ }
+}
diff --git a/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/di/AppConfigModule.kt b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/di/AppConfigModule.kt
index d58697d79c4..dc9da7f8f75 100644
--- a/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/di/AppConfigModule.kt
+++ b/WooCommerce-Wear/src/main/java/com/woocommerce/android/wear/di/AppConfigModule.kt
@@ -1,6 +1,7 @@
package com.woocommerce.android.wear.di
import android.content.Context
+import android.icu.text.CompactDecimalFormat
import com.woocommerce.android.BuildConfig
import com.woocommerce.android.wear.system.ConnectionStatus
import com.woocommerce.android.wear.ui.login.LoginRepository
@@ -35,4 +36,10 @@ class AppConfigModule {
@Provides
fun provideDefaultLocale(): Locale = Locale.getDefault()
+
+ @Provides
+ fun provideCompactDecimalFormat(locale: Locale) = CompactDecimalFormat.getInstance(
+ locale,
+ CompactDecimalFormat.CompactStyle.SHORT
+ )
}
diff --git a/WooCommerce-Wear/src/main/res/drawable/img_woo_bubble_monochrome.xml b/WooCommerce-Wear/src/main/res/drawable/img_woo_bubble_monochrome.xml
new file mode 100644
index 00000000000..00bfd189dad
--- /dev/null
+++ b/WooCommerce-Wear/src/main/res/drawable/img_woo_bubble_monochrome.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/WooCommerce-Wear/src/main/res/values/colors_base.xml b/WooCommerce-Wear/src/main/res/values/colors_base.xml
index 3e368813626..1cf31636763 100644
--- a/WooCommerce-Wear/src/main/res/values/colors_base.xml
+++ b/WooCommerce-Wear/src/main/res/values/colors_base.xml
@@ -110,7 +110,7 @@
@color/woo_black_90_alpha_012
@color/woo_black_90_alpha_012
@color/woo_black_90_alpha_012
- @color/woo_purple_60
+ @color/woo_black
@color/woo_white
@color/color_surface
diff --git a/WooCommerce-Wear/src/main/res/values/strings.xml b/WooCommerce-Wear/src/main/res/values/strings.xml
index b3acd4e2440..ad497504f0f 100644
--- a/WooCommerce-Wear/src/main/res/values/strings.xml
+++ b/WooCommerce-Wear/src/main/res/values/strings.xml
@@ -6,7 +6,7 @@
Connecting
Log in
- Log in to Woo on your phone and hold your Watch nearby
+ Log in to Woo on your phone and hold it nearby
It\'s not working
@@ -32,4 +32,13 @@
App Shortcut
App Shortcut
+ Order Totals
+ $42k
+ Display your store today\'s order totals
+ Order Count
+ 7
+ Display your store today\'s order count
+ Visitors Count
+ 42
+ Display your store today\'s visitors count
diff --git a/WooCommerce-Wear/src/main/res/values/styles.xml b/WooCommerce-Wear/src/main/res/values/styles.xml
index 915888c6505..f69a70a5f00 100644
--- a/WooCommerce-Wear/src/main/res/values/styles.xml
+++ b/WooCommerce-Wear/src/main/res/values/styles.xml
@@ -2,7 +2,7 @@
diff --git a/WooCommerce-Wear/src/test/java/com/woocommerce/android/wear/complications/FetchStatsForComplicationsTest.kt b/WooCommerce-Wear/src/test/java/com/woocommerce/android/wear/complications/FetchStatsForComplicationsTest.kt
new file mode 100644
index 00000000000..eff47e28b4c
--- /dev/null
+++ b/WooCommerce-Wear/src/test/java/com/woocommerce/android/wear/complications/FetchStatsForComplicationsTest.kt
@@ -0,0 +1,111 @@
+package com.woocommerce.android.wear.complications
+
+import android.icu.text.CompactDecimalFormat
+import com.woocommerce.android.BaseUnitTest
+import com.woocommerce.android.wear.ui.login.LoginRepository
+import com.woocommerce.android.wear.ui.stats.datasource.StatsRepository
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Before
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.fluxc.model.WCRevenueStatsModel
+import kotlin.test.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class FetchStatsForComplicationsTest : BaseUnitTest() {
+ private lateinit var sut: FetchStatsForComplications
+ private val coroutineScope: CoroutineScope = TestScope(coroutinesTestRule.testDispatcher)
+ private val statsRepository: StatsRepository = mock()
+ private val loginRepository: LoginRepository = mock()
+ private val decimalFormat: CompactDecimalFormat = mock()
+
+ @Before
+ fun setUp() {
+ sut = FetchStatsForComplications(coroutineScope, statsRepository, loginRepository, decimalFormat)
+ }
+
+ @Test
+ fun `returns formatted order totals when site is selected`() = testBlocking {
+ val site = SiteModel()
+ val total = mock {
+ on { totalSales } doReturn 100.0
+ }
+ val revenueStats = mock {
+ on { parseTotal() } doReturn total
+ }
+ whenever(loginRepository.selectedSiteFlow).thenReturn(MutableStateFlow(site))
+ whenever(statsRepository.fetchRevenueStats(site)).thenReturn(Result.success(revenueStats))
+ whenever(decimalFormat.format(100.0)).thenReturn("100")
+
+ val result = sut(FetchStatsForComplications.StatType.ORDER_TOTALS)
+
+ assertThat(result).isEqualTo("100")
+ }
+
+ @Test
+ fun `returns default value when order totals fetch fails`() = testBlocking {
+ val site = SiteModel()
+ whenever(loginRepository.selectedSiteFlow).thenReturn(MutableStateFlow(site))
+ whenever(statsRepository.fetchRevenueStats(site)).thenReturn(Result.failure(Exception()))
+
+ val result = sut(FetchStatsForComplications.StatType.ORDER_TOTALS)
+
+ assertThat(result).isEqualTo(FetchStatsForComplications.DEFAULT_EMPTY_VALUE)
+ }
+
+ @Test
+ fun `returns order count when site is selected`() = testBlocking {
+ val site = SiteModel()
+ val total = mock {
+ on { ordersCount } doReturn 10
+ }
+ val revenueStats = mock {
+ on { parseTotal() } doReturn total
+ }
+ whenever(loginRepository.selectedSiteFlow).thenReturn(MutableStateFlow(site))
+ whenever(statsRepository.fetchRevenueStats(site)).thenReturn(Result.success(revenueStats))
+
+ val result = sut(FetchStatsForComplications.StatType.ORDER_COUNT)
+
+ assertThat(result).isEqualTo("10")
+ }
+
+ @Test
+ fun `returns default value when order count fetch fails`() = testBlocking {
+ val site = SiteModel()
+ whenever(loginRepository.selectedSiteFlow).thenReturn(MutableStateFlow(site))
+ whenever(statsRepository.fetchRevenueStats(site)).thenReturn(Result.failure(Exception()))
+
+ val result = sut(FetchStatsForComplications.StatType.ORDER_COUNT)
+
+ assertThat(result).isEqualTo(FetchStatsForComplications.DEFAULT_EMPTY_VALUE)
+ }
+
+ @Test
+ fun `returns visitors count when site is selected`() = testBlocking {
+ val site = SiteModel()
+ whenever(loginRepository.selectedSiteFlow).thenReturn(MutableStateFlow(site))
+ whenever(statsRepository.fetchVisitorStats(site)).thenReturn(Result.success(100))
+
+ val result = sut(FetchStatsForComplications.StatType.VISITORS_COUNT)
+
+ assertThat(result).isEqualTo("100")
+ }
+
+ @Test
+ fun `returns default value when visitors count fetch fails`() = testBlocking {
+ val site = SiteModel()
+ whenever(loginRepository.selectedSiteFlow).thenReturn(MutableStateFlow(site))
+ whenever(statsRepository.fetchVisitorStats(site)).thenReturn(Result.failure(Exception()))
+
+ val result = sut(FetchStatsForComplications.StatType.VISITORS_COUNT)
+
+ assertThat(result).isEqualTo(FetchStatsForComplications.DEFAULT_EMPTY_VALUE)
+ }
+}
diff --git a/WooCommerce/build.gradle b/WooCommerce/build.gradle
index 20f1c7292de..17c83467b85 100644
--- a/WooCommerce/build.gradle
+++ b/WooCommerce/build.gradle
@@ -213,6 +213,8 @@ android {
}
lintOptions {
+ sarifReport System.getenv('CI') ? true : false
+ checkDependencies true
disable 'InvalidPackage'
}
@@ -467,6 +469,8 @@ dependencies {
implementation "com.google.guava:guava:$guavaVersion"
implementation "com.google.protobuf:protobuf-javalite:$protobufVersion"
+
+ lintChecks "com.android.security.lint:lint:$securityLintVersion"
}
protobuf {
diff --git a/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/WooPosExitPosDialogTest.kt b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/WooPosExitPosDialogTest.kt
index f416eabaca7..1205880816e 100644
--- a/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/WooPosExitPosDialogTest.kt
+++ b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/WooPosExitPosDialogTest.kt
@@ -75,6 +75,7 @@ class WooPosExitPosDialogTest : TestBase() {
.assertIsDisplayed()
true
} catch (e: AssertionError) {
+ e.printStackTrace()
false
}
}
@@ -111,6 +112,7 @@ class WooPosExitPosDialogTest : TestBase() {
.assertIsDisplayed()
true
} catch (e: AssertionError) {
+ e.printStackTrace()
false
}
}
@@ -159,6 +161,7 @@ class WooPosExitPosDialogTest : TestBase() {
.assertIsDisplayed()
true
} catch (e: AssertionError) {
+ e.printStackTrace()
false
}
}
@@ -199,6 +202,7 @@ class WooPosExitPosDialogTest : TestBase() {
.assertIsDisplayed()
true
} catch (e: AssertionError) {
+ e.printStackTrace()
false
}
}
@@ -239,6 +243,7 @@ class WooPosExitPosDialogTest : TestBase() {
.assertIsDisplayed()
true
} catch (e: AssertionError) {
+ e.printStackTrace()
false
}
}
diff --git a/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/WooPosHomeProductInfoDialogTest.kt b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/WooPosHomeProductInfoDialogTest.kt
index 0f0020307c4..cea5e467b34 100644
--- a/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/WooPosHomeProductInfoDialogTest.kt
+++ b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/WooPosHomeProductInfoDialogTest.kt
@@ -76,6 +76,7 @@ class WooPosHomeProductInfoDialogTest : TestBase() {
.assertIsDisplayed()
true
} catch (e: AssertionError) {
+ e.printStackTrace()
false
}
}
@@ -94,8 +95,7 @@ class WooPosHomeProductInfoDialogTest : TestBase() {
}
@Test
- fun testProductInfoDialogIsDisplayedOnIconClick() = runTest {
-
+ fun testProductInfoDialogIsDisplayedOnIconClick() = runTest {
composeTestRule.waitUntil(5000) {
try {
composeTestRule.onNodeWithTag("product_list")
@@ -103,6 +103,7 @@ class WooPosHomeProductInfoDialogTest : TestBase() {
.assertIsDisplayed()
true
} catch (e: AssertionError) {
+ e.printStackTrace()
false
}
}
@@ -134,6 +135,7 @@ class WooPosHomeProductInfoDialogTest : TestBase() {
.assertIsDisplayed()
true
} catch (e: AssertionError) {
+ e.printStackTrace()
false
}
}
@@ -171,8 +173,7 @@ class WooPosHomeProductInfoDialogTest : TestBase() {
}
@Test
- fun testProductInfoDialogIsDismissedWhenCloseClick() = runTest {
-
+ fun testProductInfoDialogIsDismissedWhenCloseClick() = runTest {
composeTestRule.waitUntil(5000) {
try {
composeTestRule.onNodeWithTag("product_list")
@@ -180,6 +181,7 @@ class WooPosHomeProductInfoDialogTest : TestBase() {
.assertIsDisplayed()
true
} catch (e: AssertionError) {
+ e.printStackTrace()
false
}
}
@@ -207,8 +209,7 @@ class WooPosHomeProductInfoDialogTest : TestBase() {
}
@Test
- fun testProductInfoDialogIsDismissedWhenPrimaryButtonClicked() = runTest {
-
+ fun testProductInfoDialogIsDismissedWhenPrimaryButtonClicked() = runTest {
composeTestRule.waitUntil(5000) {
try {
composeTestRule.onNodeWithTag("product_list")
@@ -216,6 +217,7 @@ class WooPosHomeProductInfoDialogTest : TestBase() {
.assertIsDisplayed()
true
} catch (e: AssertionError) {
+ e.printStackTrace()
false
}
}
@@ -226,7 +228,7 @@ class WooPosHomeProductInfoDialogTest : TestBase() {
composeTestRule.waitUntil(5000) {
composeTestRule.onNodeWithContentDescription(
composeTestRule.activity.getString(R.string.woopos_banner_simple_products_info_content_description)
- )
+ )
.assertExists()
true
}
@@ -247,41 +249,4 @@ class WooPosHomeProductInfoDialogTest : TestBase() {
true
}
}
-
- @Test
- fun testProductInfoDialogIsDismissedWhenOutsideDialogClicked() = runTest {
-
- composeTestRule.waitUntil(5000) {
- try {
- composeTestRule.onNodeWithTag("product_list")
- .assertExists()
- .assertIsDisplayed()
- true
- } catch (e: AssertionError) {
- false
- }
- }
- composeTestRule.onNodeWithContentDescription(
- composeTestRule.activity.getString(R.string.woopos_banner_simple_products_close_content_description)
- ).performClick()
-
- composeTestRule.waitUntil(5000) {
- composeTestRule.onNodeWithContentDescription(
- composeTestRule.activity.getString(R.string.woopos_banner_simple_products_info_content_description)
- ).performClick()
- true
- }
-
- composeTestRule.waitUntil(5000) {
- composeTestRule.onNodeWithTag("woo_pos_product_info_dialog_background")
- .performClick()
- true
- }
-
- composeTestRule.waitUntil(5000) {
- composeTestRule.onNodeWithTag("woo_pos_product_info_dialog")
- .assertIsNotDisplayed()
- true
- }
- }
}
diff --git a/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/WooPosProductScreenTest.kt b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/WooPosProductScreenTest.kt
new file mode 100644
index 00000000000..2b42baa5c24
--- /dev/null
+++ b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/WooPosProductScreenTest.kt
@@ -0,0 +1,494 @@
+@file:Suppress("DEPRECATION")
+
+package com.woocommerce.android
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.isNotDisplayed
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.rule.ActivityTestRule
+import com.woocommerce.android.e2e.helpers.InitializationRule
+import com.woocommerce.android.e2e.helpers.TestBase
+import com.woocommerce.android.e2e.helpers.util.MocksReader
+import com.woocommerce.android.e2e.helpers.util.ProductData
+import com.woocommerce.android.e2e.helpers.util.iterator
+import com.woocommerce.android.e2e.rules.RetryTestRule
+import com.woocommerce.android.e2e.screens.TabNavComponent
+import com.woocommerce.android.e2e.screens.login.WelcomeScreen
+import com.woocommerce.android.ui.login.LoginActivity
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import kotlinx.coroutines.test.runTest
+import org.json.JSONObject
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import javax.inject.Inject
+
+@HiltAndroidTest
+@RunWith(AndroidJUnit4::class)
+class WooPosProductScreenTest : TestBase() {
+ @get:Rule(order = 1)
+ val rule = HiltAndroidRule(this)
+
+ @get:Rule(order = 2)
+ val composeTestRule = createAndroidComposeRule()
+
+ @get:Rule(order = 3)
+ val initRule = InitializationRule()
+
+ @get:Rule(order = 4)
+ var activityRule = ActivityTestRule(LoginActivity::class.java)
+
+ @get:Rule(order = 5)
+ var retryTestRule = RetryTestRule()
+
+ @Inject
+ lateinit var dataStore: DataStore
+
+ @Before
+ fun setUp() = runTest {
+ rule.inject()
+ dataStore.edit { it.clear() }
+ WelcomeScreen
+ .skipCarouselIfNeeded()
+ .selectLogin()
+ .proceedWith(BuildConfig.SCREENSHOTS_URL)
+ .proceedWith(BuildConfig.SCREENSHOTS_USERNAME)
+ .proceedWith(BuildConfig.SCREENSHOTS_PASSWORD)
+
+ TabNavComponent()
+ .gotoMoreMenuScreen()
+ .openPOSScreen(composeTestRule)
+ }
+
+ @Test
+ fun testProductScreenIsDisplayedWhenPosModeEntered() = runTest {
+ composeTestRule.waitUntil(5000) {
+ try {
+ composeTestRule.onNodeWithTag("product_list")
+ .assertExists()
+ .assertIsDisplayed()
+ true
+ } catch (e: AssertionError) {
+ e.printStackTrace()
+ false
+ }
+ }
+ }
+
+ @Test
+ fun testCartItemIsAddedWhenProductItemClicked() = runTest {
+ val productsJSONArray = MocksReader().readAllProductsToArray()
+ val products = mutableListOf()
+ for (productJSON in productsJSONArray.iterator()) {
+ products.add(mapJSONToProduct(productJSON))
+ }
+ val firstProduct = products.first()
+ composeTestRule.waitUntil(5000) {
+ try {
+ composeTestRule.onNodeWithTag("product_list")
+ .assertExists()
+ .assertIsDisplayed()
+ true
+ } catch (e: AssertionError) {
+ e.printStackTrace()
+ false
+ }
+ }
+ // asserting the cart is empty initially
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_item_${firstProduct.name}")
+ .assertDoesNotExist()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_product_item_${firstProduct.name}").performClick()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_item_${firstProduct.name}")
+ .assertExists()
+ true
+ }
+ }
+
+ @Test
+ fun testCheckoutButtonIsNotVisibleWhenCartListIsNotDisplayed() = runTest {
+ composeTestRule.waitUntil(5000) {
+ try {
+ composeTestRule.onNodeWithTag("product_list")
+ .assertExists()
+ .assertIsDisplayed()
+ true
+ } catch (e: AssertionError) {
+ e.printStackTrace()
+ false
+ }
+ }
+ // asserting the cart is empty
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_list")
+ .assertIsNotDisplayed()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_checkout_button")
+ .assertIsNotDisplayed()
+ true
+ }
+ }
+
+ @Suppress("LongMethod")
+ @Test
+ fun testCheckoutButtonVisibility() = runTest {
+ val productsJSONArray = MocksReader().readAllProductsToArray()
+ val products = mutableListOf()
+ for (productJSON in productsJSONArray.iterator()) {
+ products.add(mapJSONToProduct(productJSON))
+ }
+ val firstProduct = products.first()
+ composeTestRule.waitUntil(5000) {
+ try {
+ composeTestRule.onNodeWithTag("product_list")
+ .assertExists()
+ .assertIsDisplayed()
+ true
+ } catch (e: AssertionError) {
+ e.printStackTrace()
+ false
+ }
+ }
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_list")
+ .assertIsNotDisplayed()
+ true
+ }
+
+ // asserting the checkout button is not displayed when the cart is empty
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_checkout_button")
+ .assertIsNotDisplayed()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_product_item_${firstProduct.name}").performClick()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_item_${firstProduct.name}")
+ .assertExists()
+ true
+ }
+
+ // asserting the checkout button is displayed when the cart has items
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_checkout_button")
+ .assertIsDisplayed()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_item_close_icon_${firstProduct.name}")
+ .performClick()
+ true
+ }
+
+ // asserting the checkout button is not displayed when the cart items are removed
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_checkout_button")
+ .assertIsNotDisplayed()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_product_item_${firstProduct.name}").performClick()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_checkout_button")
+ .performClick()
+ true
+ }
+
+ // asserting the checkout button is not displayed when we move to the checkout screen
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_checkout_button")
+ .assertIsNotDisplayed()
+ true
+ }
+ }
+
+ @Suppress("LongMethod")
+ @Test
+ fun testCartItemRemovalFunctionality() = runTest {
+ val productsJSONArray = MocksReader().readAllProductsToArray()
+ val products = mutableListOf()
+ for (productJSON in productsJSONArray.iterator()) {
+ products.add(mapJSONToProduct(productJSON))
+ }
+ val firstProduct = products.first()
+ val secondProduct = products[1]
+ val thirdProduct = products[2]
+ composeTestRule.waitUntil(5000) {
+ try {
+ composeTestRule.onNodeWithTag("product_list")
+ .assertExists()
+ .assertIsDisplayed()
+ true
+ } catch (e: AssertionError) {
+ e.printStackTrace()
+ false
+ }
+ }
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_list")
+ .assertIsNotDisplayed()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_product_item_${firstProduct.name}").performClick()
+ true
+ }
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_product_item_${secondProduct.name}").performClick()
+ true
+ }
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_product_item_${thirdProduct.name}").performClick()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_item_${firstProduct.name}")
+ .assertExists()
+ true
+ }
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_item_${secondProduct.name}")
+ .assertExists()
+ true
+ }
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_item_${thirdProduct.name}")
+ .assertExists()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_item_close_icon_${secondProduct.name}")
+ .performClick()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_item_${secondProduct.name}")
+ .isNotDisplayed()
+ true
+ }
+ }
+
+ @Test
+ fun testClickingCheckoutButtonHidesProductsScreen() = runTest {
+ val productsJSONArray = MocksReader().readAllProductsToArray()
+ val products = mutableListOf()
+ for (productJSON in productsJSONArray.iterator()) {
+ products.add(mapJSONToProduct(productJSON))
+ }
+ val firstProduct = products.first()
+ composeTestRule.waitUntil(5000) {
+ try {
+ composeTestRule.onNodeWithTag("product_list")
+ .assertExists()
+ .assertIsDisplayed()
+ true
+ } catch (e: AssertionError) {
+ e.printStackTrace()
+ false
+ }
+ }
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_list")
+ .assertIsNotDisplayed()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_product_item_${firstProduct.name}").performClick()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_item_${firstProduct.name}")
+ .assertExists()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_checkout_button")
+ .assertIsDisplayed()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_checkout_button")
+ .performClick()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("product_list")
+ .assertIsNotDisplayed()
+ true
+ }
+ }
+
+ @Test
+ fun testClickingCheckoutButtonDisplaysTotalsScreen() = runTest {
+ val productsJSONArray = MocksReader().readAllProductsToArray()
+ val products = mutableListOf()
+ for (productJSON in productsJSONArray.iterator()) {
+ products.add(mapJSONToProduct(productJSON))
+ }
+ val firstProduct = products.first()
+ composeTestRule.waitUntil(5000) {
+ try {
+ composeTestRule.onNodeWithTag("product_list")
+ .assertExists()
+ .assertIsDisplayed()
+ true
+ } catch (e: AssertionError) {
+ e.printStackTrace()
+ false
+ }
+ }
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_list")
+ .assertIsNotDisplayed()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_product_item_${firstProduct.name}").performClick()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_item_${firstProduct.name}")
+ .assertExists()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_checkout_button")
+ .assertIsDisplayed()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_checkout_button")
+ .performClick()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_totals_loaded_screen")
+ .assertIsDisplayed()
+ true
+ }
+ }
+
+ @Test
+ fun testClickingCheckoutButtonDisplaysCollectPaymentButton() = runTest {
+ val productsJSONArray = MocksReader().readAllProductsToArray()
+ val products = mutableListOf()
+ for (productJSON in productsJSONArray.iterator()) {
+ products.add(mapJSONToProduct(productJSON))
+ }
+ val firstProduct = products.first()
+ composeTestRule.waitUntil(5000) {
+ try {
+ composeTestRule.onNodeWithTag("product_list")
+ .assertExists()
+ .assertIsDisplayed()
+ true
+ } catch (e: AssertionError) {
+ e.printStackTrace()
+ false
+ }
+ }
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_list")
+ .assertIsNotDisplayed()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_product_item_${firstProduct.name}").performClick()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_cart_item_${firstProduct.name}")
+ .assertExists()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_checkout_button")
+ .assertIsDisplayed()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_checkout_button")
+ .performClick()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithTag("woo_pos_totals_loaded_screen")
+ .assertIsDisplayed()
+ true
+ }
+
+ composeTestRule.waitUntil(5000) {
+ composeTestRule.onNodeWithText(
+ composeTestRule.activity.getString(
+ R.string.woopos_payment_collect_payment_label
+ )
+ )
+ .assertIsDisplayed()
+ true
+ }
+ }
+
+ private fun mapJSONToProduct(productJSON: JSONObject): ProductData {
+ return ProductData(
+ id = productJSON.getInt("id"),
+ name = productJSON.getString("name"),
+ stockStatusRaw = productJSON.getString("stock_status"),
+ priceDiscountedRaw = productJSON.getString("price"),
+ priceRegularRaw = productJSON.getString("regular_price"),
+ typeRaw = productJSON.getString("type"),
+ rating = productJSON.getInt("average_rating"),
+ reviewsCount = productJSON.getInt("rating_count")
+ )
+ }
+}
diff --git a/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/e2e/screens/notifications/NotificationsScreen.kt b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/e2e/screens/notifications/NotificationsScreen.kt
index 0785b8ca0db..fea3ae0e5b6 100644
--- a/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/e2e/screens/notifications/NotificationsScreen.kt
+++ b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/e2e/screens/notifications/NotificationsScreen.kt
@@ -8,7 +8,7 @@ import com.woocommerce.android.e2e.screens.TabNavComponent
import com.woocommerce.android.model.Notification
import com.woocommerce.android.notifications.NotificationChannelType
import com.woocommerce.android.notifications.WooNotificationBuilder
-import com.woocommerce.android.notifications.WooNotificationType.NEW_ORDER
+import com.woocommerce.android.notifications.WooNotificationType.NewOrder
/**
* This is not a screen per-se, as it shows the notification drawer with a push notification.
@@ -32,7 +32,7 @@ class NotificationsScreen(private val wooNotificationBuilder: WooNotificationBui
icon = "https://s.wp.com/wp-content/mu-plugins/notes/images/update-payment-2x.png",
noteTitle = getTranslatedString(R.string.tests_notification_new_order_title),
noteMessage = getTranslatedString(R.string.tests_notification_new_order_message),
- noteType = NEW_ORDER,
+ noteType = NewOrder,
channelType = NotificationChannelType.NEW_ORDER
),
isGroupNotification = false
diff --git a/WooCommerce/src/main/AndroidManifest.xml b/WooCommerce/src/main/AndroidManifest.xml
index 33512e09e0f..6b96d630962 100644
--- a/WooCommerce/src/main/AndroidManifest.xml
+++ b/WooCommerce/src/main/AndroidManifest.xml
@@ -23,6 +23,10 @@
+
+
+
+
Comparable?.lesserThan(other: T) =
this?.let { it < other }
?: false
+/**
+ * Shortens a number to the nearest thousand and appends a 'K' to the end. For example, 1000 will be shortened to 1K.
+ */
+fun compactNumberCompat(number: Long, locale: Locale = Locale.getDefault()): String =
+ if (VERSION.SDK_INT >= VERSION_CODES.R) {
+ NumberFormatter.with()
+ .notation(Notation.compactShort())
+ .locale(locale)
+ .format(number)
+ .toString()
+ } else {
+ CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT)
+ .format(number.toDouble())
+ }
+
const val PERCENTAGE_BASE = 100.0
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/extensions/NumberExtensionsWrapper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/extensions/NumberExtensionsWrapper.kt
new file mode 100644
index 00000000000..f655873a018
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/extensions/NumberExtensionsWrapper.kt
@@ -0,0 +1,12 @@
+package com.woocommerce.android.extensions
+
+import java.util.Locale
+import javax.inject.Inject
+import com.woocommerce.android.extensions.compactNumberCompat as compactNumberCompatExt
+
+class NumberExtensionsWrapper @Inject constructor() {
+ fun compactNumberCompat(
+ number: Long,
+ locale: Locale = Locale.getDefault()
+ ): String = compactNumberCompatExt(number, locale)
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/media/ProductImagesUploadWorker.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/media/ProductImagesUploadWorker.kt
index 78c2fc15b09..1a604bb58fa 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/media/ProductImagesUploadWorker.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/media/ProductImagesUploadWorker.kt
@@ -242,7 +242,7 @@ class ProductImagesUploadWorker @Inject constructor(
suspend fun fetchProductWithRetries(productId: Long): Product? {
var retries = 0
while (retries < PRODUCT_UPDATE_RETRIES) {
- val product = productDetailRepository.fetchProductOrLoadFromCache(productId)
+ val product = productDetailRepository.fetchAndGetProduct(productId)
if (product != null && productDetailRepository.lastFetchProductErrorType == null) {
return product
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/mediapicker/MediaPickerDialogScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/mediapicker/MediaPickerDialogScreen.kt
index 9c2fed86640..bc2a15ea884 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/mediapicker/MediaPickerDialogScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/mediapicker/MediaPickerDialogScreen.kt
@@ -35,7 +35,9 @@ import org.wordpress.android.mediapicker.api.MediaPickerSetup.DataSource.WP_MEDI
@Composable
fun MediaPickerDialog(
onDismissRequest: () -> Unit,
- onMediaLibraryRequested: (DataSource) -> Unit
+ onMediaLibraryRequested: (DataSource) -> Unit,
+ withProductImagePicker: Boolean = false,
+ onProductImagesRequested: () -> Unit = {}
) {
Dialog(onDismissRequest = onDismissRequest) {
Card(
@@ -74,6 +76,13 @@ fun MediaPickerDialog(
title = string.image_source_wp_media_library,
onClick = { onMediaLibraryRequested(WP_MEDIA_LIBRARY) }
)
+ if (withProductImagePicker) {
+ DialogButton(
+ image = drawable.ic_product,
+ title = string.image_source_product_images,
+ onClick = onProductImagesRequested
+ )
+ }
}
}
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Notification.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Notification.kt
index c48bcdb5ef5..622ad2a574e 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Notification.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Notification.kt
@@ -26,10 +26,13 @@ data class Notification(
val data: String? = null
) : Parcelable {
@IgnoredOnParcel
- val isOrderNotification = noteType == WooNotificationType.NEW_ORDER
+ val isOrderNotification = noteType is WooNotificationType.NewOrder
@IgnoredOnParcel
- val isReviewNotification = noteType == WooNotificationType.PRODUCT_REVIEW
+ val isReviewNotification = noteType is WooNotificationType.ProductReview
+
+ @IgnoredOnParcel
+ val isBlazeNotification = noteType is WooNotificationType.BlazeStatusUpdate
/**
* Notifications are grouped based on the notification type and the store the notification belongs to.
@@ -74,6 +77,11 @@ fun NotificationModel.getUniqueId(): Long {
return when (this.type) {
NotificationModel.Kind.STORE_ORDER -> this.meta?.ids?.order ?: 0L
NotificationModel.Kind.COMMENT -> this.meta?.ids?.comment ?: 0L
+ NotificationModel.Kind.BLAZE_APPROVED_NOTE,
+ NotificationModel.Kind.BLAZE_REJECTED_NOTE,
+ NotificationModel.Kind.BLAZE_CANCELLED_NOTE,
+ NotificationModel.Kind.BLAZE_PERFORMED_NOTE -> this.meta?.ids?.campaignId ?: 0L
+
else -> 0L
}
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Product.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Product.kt
index ef373bb37c3..c27fc2aac06 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Product.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Product.kt
@@ -92,7 +92,8 @@ data class Product(
val bundleMinSize: Float?,
val bundleMaxSize: Float?,
val groupOfQuantity: Int?,
- val combineVariationQuantities: Boolean?
+ val combineVariationQuantities: Boolean?,
+ val password: String?
) : Parcelable, IProduct {
companion object {
const val TAX_CLASS_DEFAULT = "standard"
@@ -157,7 +158,8 @@ data class Product(
minAllowedQuantity == product.minAllowedQuantity &&
maxAllowedQuantity == product.maxAllowedQuantity &&
groupOfQuantity == product.groupOfQuantity &&
- combineVariationQuantities == product.combineVariationQuantities
+ combineVariationQuantities == product.combineVariationQuantities &&
+ password == product.password
}
val hasCategories get() = categories.isNotEmpty()
@@ -479,6 +481,7 @@ fun Product.toDataModel(storedProductModel: WCProductModel? = null): WCProductMo
it.maxAllowedQuantity = maxAllowedQuantity ?: -1
it.groupOfQuantity = groupOfQuantity ?: -1
it.combineVariationQuantities = combineVariationQuantities ?: false
+ it.password = password
// Subscription details are currently the only editable metadata fields from the app.
it.metadata = subscription?.toMetadataJson().toString()
}
@@ -590,7 +593,8 @@ fun WCProductModel.toAppModel(): Product {
bundleMinSize = this.bundleMinSize,
bundleMaxSize = this.bundleMaxSize,
groupOfQuantity = this.groupOfQuantity(),
- combineVariationQuantities = this.combineVariationQuantities
+ combineVariationQuantities = this.combineVariationQuantities,
+ password = this.password
)
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/WooNotificationType.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/WooNotificationType.kt
index 23d50cf95ae..3f566037040 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/WooNotificationType.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/WooNotificationType.kt
@@ -1,18 +1,60 @@
package com.woocommerce.android.notifications
+import android.os.Parcelable
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize
import org.wordpress.android.fluxc.model.notification.NotificationModel
-enum class WooNotificationType {
- NEW_ORDER,
- PRODUCT_REVIEW,
- LOCAL_REMINDER,
- BLAZE
+sealed interface WooNotificationType : Parcelable {
+ val trackingValue: String
+
+ @Parcelize
+ data object NewOrder : WooNotificationType {
+ @IgnoredOnParcel override val trackingValue: String = "NEW_ORDER"
+ }
+
+ @Parcelize
+ data object ProductReview : WooNotificationType {
+ @IgnoredOnParcel override val trackingValue: String = "PRODUCT_REVIEW"
+ }
+
+ @Parcelize
+ data object LocalReminder : WooNotificationType {
+ @IgnoredOnParcel override val trackingValue: String = "LOCAL_REMINDER"
+ }
+
+ @Parcelize
+ sealed interface BlazeStatusUpdate : WooNotificationType, Parcelable {
+ @Parcelize
+ data object BlazeApprovedNote : BlazeStatusUpdate {
+ @IgnoredOnParcel override val trackingValue: String = "blaze_approved_note"
+ }
+
+ @Parcelize
+ data object BlazeRejectedNote : BlazeStatusUpdate {
+ @IgnoredOnParcel override val trackingValue: String = "blaze_rejected_note"
+ }
+
+ @Parcelize
+ data object BlazeCancelledNote : BlazeStatusUpdate {
+ @IgnoredOnParcel override val trackingValue: String = "blaze_cancelled_note"
+ }
+
+ @Parcelize
+ data object BlazePerformedNote : BlazeStatusUpdate {
+ @IgnoredOnParcel override val trackingValue: String = "blaze_performed_note"
+ }
+ }
}
fun NotificationModel.getWooType(): WooNotificationType {
return when (this.type) {
- NotificationModel.Kind.STORE_ORDER -> WooNotificationType.NEW_ORDER
- NotificationModel.Kind.COMMENT -> WooNotificationType.PRODUCT_REVIEW
- else -> WooNotificationType.LOCAL_REMINDER
+ NotificationModel.Kind.STORE_ORDER -> WooNotificationType.NewOrder
+ NotificationModel.Kind.COMMENT -> WooNotificationType.ProductReview
+ NotificationModel.Kind.BLAZE_APPROVED_NOTE -> WooNotificationType.BlazeStatusUpdate.BlazeApprovedNote
+ NotificationModel.Kind.BLAZE_REJECTED_NOTE -> WooNotificationType.BlazeStatusUpdate.BlazeRejectedNote
+ NotificationModel.Kind.BLAZE_CANCELLED_NOTE -> WooNotificationType.BlazeStatusUpdate.BlazeCancelledNote
+ NotificationModel.Kind.BLAZE_PERFORMED_NOTE -> WooNotificationType.BlazeStatusUpdate.BlazePerformedNote
+ else -> WooNotificationType.LocalReminder
}
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotification.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotification.kt
index 2b88ed02bcd..f39613bb68e 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotification.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotification.kt
@@ -16,9 +16,6 @@ sealed class LocalNotification(
open val data: String? = null
val id = type.hashCode()
- val tag
- get() = "$type:$siteId"
-
fun getTitleString(resourceProvider: ResourceProvider) = resourceProvider.getString(title)
fun getDescriptionString(resourceProvider: ResourceProvider) = resourceProvider.getString(description)
@@ -34,4 +31,13 @@ sealed class LocalNotification(
delay = delay,
delayUnit = TimeUnit.MILLISECONDS
)
+
+ data class BlazeAbandonedCampaignReminderNotification(override val siteId: Long) : LocalNotification(
+ siteId = siteId,
+ title = R.string.local_notification_blaze_abandoned_campaign_reminder_title,
+ description = R.string.local_notification_blaze_abandoned_campaign_reminder_description,
+ type = LocalNotificationType.BLAZE_ABANDONED_CAMPAIGN_REMINDER,
+ delay = 1,
+ delayUnit = TimeUnit.DAYS
+ )
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotificationScheduler.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotificationScheduler.kt
index ab208f2f25d..895275c3625 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotificationScheduler.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotificationScheduler.kt
@@ -8,6 +8,7 @@ import androidx.work.WorkManager
import androidx.work.workDataOf
import com.woocommerce.android.analytics.AnalyticsEvent
import com.woocommerce.android.analytics.AnalyticsTracker
+import com.woocommerce.android.util.WooLog
import com.woocommerce.android.viewmodel.ResourceProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
@@ -29,11 +30,11 @@ class LocalNotificationScheduler @Inject constructor(
private val workManager = WorkManager.getInstance(appContext)
fun scheduleNotification(notification: LocalNotification) {
- cancelScheduledNotification(notification.tag)
+ cancelScheduledNotification(notification.type)
workManager
.beginUniqueWork(
- LOCAL_NOTIFICATION_WORK_NAME + notification.tag,
+ LOCAL_NOTIFICATION_WORK_NAME + notification.type.value,
REPLACE,
buildPreconditionCheckWorkRequest(notification)
)
@@ -47,6 +48,11 @@ class LocalNotificationScheduler @Inject constructor(
AnalyticsTracker.KEY_BLOG_ID to notification.siteId,
)
)
+ WooLog.d(
+ tag = WooLog.T.NOTIFICATIONS,
+ message = "Local notification scheduled: " +
+ "type=${notification.type}, delay=${notification.delay}${notification.delayUnit}"
+ )
}
private fun buildPreconditionCheckWorkRequest(notification: LocalNotification): OneTimeWorkRequest {
@@ -57,7 +63,7 @@ class LocalNotificationScheduler @Inject constructor(
)
return OneTimeWorkRequestBuilder()
.setInputData(conditionData)
- .addTag(notification.tag)
+ .addTag(notification.type.value)
.setInitialDelay(notification.delay, notification.delayUnit)
.build()
}
@@ -72,12 +78,16 @@ class LocalNotificationScheduler @Inject constructor(
LOCAL_NOTIFICATION_SITE_ID to notification.siteId
)
return OneTimeWorkRequestBuilder()
- .addTag(notification.tag)
+ .addTag(notification.type.value)
.setInputData(notificationData)
.build()
}
- private fun cancelScheduledNotification(tag: String) {
- workManager.cancelAllWorkByTag(tag)
+ fun cancelScheduledNotification(type: LocalNotificationType) {
+ workManager.cancelAllWorkByTag(type.value)
+ WooLog.d(
+ tag = WooLog.T.NOTIFICATIONS,
+ message = "Local notification canceled: $type"
+ )
}
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotificationType.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotificationType.kt
index 55999bca86a..6d43f782c12 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotificationType.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotificationType.kt
@@ -1,7 +1,8 @@
package com.woocommerce.android.notifications.local
enum class LocalNotificationType(val value: String) {
- BLAZE_NO_CAMPAIGN_REMINDER("blaze_no_campaign_reminder");
+ BLAZE_NO_CAMPAIGN_REMINDER("blaze_no_campaign_reminder"),
+ BLAZE_ABANDONED_CAMPAIGN_REMINDER("blaze_abandoned_campaign_reminder");
override fun toString() = value
companion object {
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotificationWorker.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotificationWorker.kt
index 0ea5353bc1b..93804afe187 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotificationWorker.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/LocalNotificationWorker.kt
@@ -11,7 +11,7 @@ import com.woocommerce.android.analytics.AnalyticsTracker
import com.woocommerce.android.model.Notification
import com.woocommerce.android.notifications.NotificationChannelType.OTHER
import com.woocommerce.android.notifications.WooNotificationBuilder
-import com.woocommerce.android.notifications.WooNotificationType.LOCAL_REMINDER
+import com.woocommerce.android.notifications.WooNotificationType.LocalReminder
import com.woocommerce.android.notifications.local.LocalNotificationScheduler.Companion.LOCAL_NOTIFICATION_DATA
import com.woocommerce.android.notifications.local.LocalNotificationScheduler.Companion.LOCAL_NOTIFICATION_DESC
import com.woocommerce.android.notifications.local.LocalNotificationScheduler.Companion.LOCAL_NOTIFICATION_ID
@@ -49,7 +49,7 @@ class LocalNotificationWorker @AssistedInject constructor(
notificationTappedIntent = getIntent(notification),
)
- setNotificationShown(type, siteId)
+ setNotificationShown(type)
AnalyticsTracker.track(
LOCAL_NOTIFICATION_DISPLAYED,
@@ -71,10 +71,15 @@ class LocalNotificationWorker @AssistedInject constructor(
}
}
- private fun setNotificationShown(type: String, siteId: Long) {
+ private fun setNotificationShown(type: String) {
when (LocalNotificationType.fromString(type)) {
LocalNotificationType.BLAZE_NO_CAMPAIGN_REMINDER -> {
- appsPrefsWrapper.setBlazeNoCampaignReminderShown(siteId)
+ appsPrefsWrapper.isBlazeNoCampaignReminderShown = true
+ appsPrefsWrapper.removeBlazeFirstTimeWithoutCampaign()
+ }
+
+ LocalNotificationType.BLAZE_ABANDONED_CAMPAIGN_REMINDER -> {
+ appsPrefsWrapper.isBlazeAbandonedCampaignReminderShown = true
}
else -> {}
@@ -98,7 +103,7 @@ class LocalNotificationWorker @AssistedInject constructor(
icon = null,
noteTitle = title,
noteMessage = description,
- noteType = LOCAL_REMINDER,
+ noteType = LocalReminder,
channelType = OTHER,
data = data
)
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/PreconditionCheckWorker.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/PreconditionCheckWorker.kt
index 8803f5d1596..dee3c357070 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/PreconditionCheckWorker.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/local/PreconditionCheckWorker.kt
@@ -35,7 +35,8 @@ class PreconditionCheckWorker @AssistedInject constructor(
val type = LocalNotificationType.fromString(inputData.getString(LOCAL_NOTIFICATION_TYPE))
val siteId = inputData.getLong(LOCAL_NOTIFICATION_SITE_ID, 0L)
return when (type) {
- LocalNotificationType.BLAZE_NO_CAMPAIGN_REMINDER -> proceedIfValidSiteAndBlazeAvailable(siteId)
+ LocalNotificationType.BLAZE_NO_CAMPAIGN_REMINDER,
+ LocalNotificationType.BLAZE_ABANDONED_CAMPAIGN_REMINDER -> proceedIfValidSiteAndBlazeAvailable(siteId)
null -> cancelWork("Notification type is null. Cancelling work.")
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/push/NotificationAnalyticsTracker.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/push/NotificationAnalyticsTracker.kt
index b25a96054f8..5a92827de51 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/push/NotificationAnalyticsTracker.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/push/NotificationAnalyticsTracker.kt
@@ -18,7 +18,7 @@ class NotificationAnalyticsTracker @Inject constructor(
val isFromSelectedSite = selectedSite.getIfExists()?.siteId == notification.remoteSiteId
val properties = mutableMapOf()
properties["notification_note_id"] = notification.remoteNoteId
- properties["notification_type"] = notification.noteType.name
+ properties["notification_type"] = notification.noteType.trackingValue
properties["push_notification_token"] = appPrefsWrapper.getFCMToken()
properties["is_from_selected_site"] = isFromSelectedSite == true
analyticsTrackerWrapper.track(stat, properties)
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/push/NotificationMessageHandler.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/push/NotificationMessageHandler.kt
index 05b0cb07713..4bb5d922b9a 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/push/NotificationMessageHandler.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/notifications/push/NotificationMessageHandler.kt
@@ -13,7 +13,7 @@ import com.woocommerce.android.model.isOrderNotification
import com.woocommerce.android.model.toAppModel
import com.woocommerce.android.notifications.NotificationChannelType
import com.woocommerce.android.notifications.WooNotificationBuilder
-import com.woocommerce.android.notifications.WooNotificationType.NEW_ORDER
+import com.woocommerce.android.notifications.WooNotificationType.NewOrder
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.util.NotificationsParser
import com.woocommerce.android.util.WooLog.T.NOTIFS
@@ -52,10 +52,12 @@ class NotificationMessageHandler @Inject constructor(
private val ACTIVE_NOTIFICATIONS_MAP = mutableMapOf()
}
+ @Synchronized
fun onPushNotificationDismissed(notificationId: Int) {
removeNotificationByNotificationIdFromSystemsBar(notificationId)
}
+ @Synchronized
fun onLocalNotificationDismissed(notificationId: Int, notificationType: String) {
removeNotificationByNotificationIdFromSystemsBar(notificationId)
AnalyticsTracker.track(
@@ -128,7 +130,7 @@ class NotificationMessageHandler @Inject constructor(
}
private fun handleWooNotification(notification: Notification) {
- val randomNumber = if (notification.noteType == NEW_ORDER) Random.nextInt() else 0
+ val randomNumber = if (notification.noteType == NewOrder) Random.nextInt() else 0
val localPushId = getLocalPushIdForNoteId(notification.remoteNoteId, randomNumber)
ACTIVE_NOTIFICATIONS_MAP[getLocalPushId(localPushId, randomNumber)] = notification
if (notificationBuilder.isNotificationsEnabled()) {
@@ -221,6 +223,7 @@ class NotificationMessageHandler @Inject constructor(
notificationBuilder.cancelAllNotifications()
}
+ @Synchronized
fun removeNotificationByRemoteIdFromSystemsBar(remoteNoteId: Long) {
val keptNotifs = HashMap()
ACTIVE_NOTIFICATIONS_MAP.asSequence()
@@ -237,6 +240,7 @@ class NotificationMessageHandler @Inject constructor(
updateNotificationsState()
}
+ @Synchronized
fun removeNotificationByNotificationIdFromSystemsBar(localPushId: Int) {
val keptNotifs = HashMap()
ACTIVE_NOTIFICATIONS_MAP.asSequence()
@@ -253,6 +257,7 @@ class NotificationMessageHandler @Inject constructor(
updateNotificationsState()
}
+ @Synchronized
fun removeNotificationsOfTypeFromSystemsBar(type: NotificationChannelType, remoteSiteId: Long) {
val keptNotifs = HashMap()
// Using a copy of the map to avoid concurrency problems
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/hub/sync/AnalyticsUpdateDataStore.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/hub/sync/AnalyticsUpdateDataStore.kt
index dd835112d22..b9ed4bac4d6 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/hub/sync/AnalyticsUpdateDataStore.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/hub/sync/AnalyticsUpdateDataStore.kt
@@ -122,7 +122,7 @@ class AnalyticsUpdateDataStore @Inject constructor(
true
}
}
- .map { lastUpdateValues -> lastUpdateValues.min() }
+ .map { lastUpdateValues -> lastUpdateValues.minOrNull() }
}
private fun observeLastUpdate(timestampKey: String): Flow {
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/appwidgets/IsDeviceBatterySaverActive.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/appwidgets/IsDeviceBatterySaverActive.kt
new file mode 100644
index 00000000000..9ea3b148de4
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/appwidgets/IsDeviceBatterySaverActive.kt
@@ -0,0 +1,16 @@
+package com.woocommerce.android.ui.appwidgets
+
+import android.content.Context
+import android.os.PowerManager
+import javax.inject.Inject
+
+class IsDeviceBatterySaverActive @Inject constructor(
+ private val appContext: Context
+) {
+ operator fun invoke(): Boolean {
+ return appContext.getSystemService(Context.POWER_SERVICE)
+ .run { this as? PowerManager }
+ ?.isPowerSaveMode
+ ?: false
+ }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/appwidgets/stats/GetWidgetStats.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/appwidgets/stats/GetWidgetStats.kt
index 34a843dc76b..514d57d8e96 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/appwidgets/stats/GetWidgetStats.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/appwidgets/stats/GetWidgetStats.kt
@@ -7,6 +7,12 @@ import com.woocommerce.android.tools.connectionType
import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection
import com.woocommerce.android.ui.analytics.ranges.revenueStatsGranularity
import com.woocommerce.android.ui.analytics.ranges.visitorStatsGranularity
+import com.woocommerce.android.ui.appwidgets.IsDeviceBatterySaverActive
+import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats.WidgetStatsResult.WidgetStatsAPINotSupportedFailure
+import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats.WidgetStatsResult.WidgetStatsAuthFailure
+import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats.WidgetStatsResult.WidgetStatsBatterySaverActive
+import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats.WidgetStatsResult.WidgetStatsFailure
+import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats.WidgetStatsResult.WidgetStatsNetworkFailure
import com.woocommerce.android.ui.dashboard.data.StatsRepository
import com.woocommerce.android.ui.login.AccountRepository
import com.woocommerce.android.util.CoroutineDispatchers
@@ -20,6 +26,7 @@ class GetWidgetStats @Inject constructor(
private val appPrefsWrapper: AppPrefsWrapper,
private val statsRepository: StatsRepository,
private val coroutineDispatchers: CoroutineDispatchers,
+ private val isDeviceBatterySaverActive: IsDeviceBatterySaverActive,
private val networkStatus: NetworkStatus
) {
suspend operator fun invoke(
@@ -28,14 +35,15 @@ class GetWidgetStats @Inject constructor(
): WidgetStatsResult {
return withContext(coroutineDispatchers.io) {
when {
+ isDeviceBatterySaverActive() -> WidgetStatsBatterySaverActive
// If user is not logged in, exit the function with WidgetStatsAuthFailure
- accountRepository.isUserLoggedIn().not() -> WidgetStatsResult.WidgetStatsAuthFailure
+ accountRepository.isUserLoggedIn().not() -> WidgetStatsAuthFailure
// If V4 stats is not supported, exit the function with WidgetStatsAPINotSupportedFailure
- appPrefsWrapper.isV4StatsSupported().not() -> WidgetStatsResult.WidgetStatsAPINotSupportedFailure
+ appPrefsWrapper.isV4StatsSupported().not() -> WidgetStatsAPINotSupportedFailure
// If network is not available, exit the function with WidgetStatsNetworkFailure
- networkStatus.isConnected().not() -> WidgetStatsResult.WidgetStatsNetworkFailure
+ networkStatus.isConnected().not() -> WidgetStatsNetworkFailure
// If siteModel is null, exit the function with WidgetStatsFailure
- siteModel == null -> WidgetStatsResult.WidgetStatsFailure("No site selected")
+ siteModel == null -> WidgetStatsFailure("No site selected")
else -> {
val areVisitorStatsSupported = siteModel.connectionType == SiteConnectionType.Jetpack
@@ -53,7 +61,7 @@ class GetWidgetStats @Inject constructor(
WidgetStatsResult.WidgetStats(stats)
},
onFailure = { error ->
- WidgetStatsResult.WidgetStatsFailure(error.message)
+ WidgetStatsFailure(error.message)
}
)
}
@@ -62,9 +70,10 @@ class GetWidgetStats @Inject constructor(
}
sealed class WidgetStatsResult {
- object WidgetStatsAuthFailure : WidgetStatsResult()
- object WidgetStatsNetworkFailure : WidgetStatsResult()
- object WidgetStatsAPINotSupportedFailure : WidgetStatsResult()
+ data object WidgetStatsBatterySaverActive : WidgetStatsResult()
+ data object WidgetStatsAuthFailure : WidgetStatsResult()
+ data object WidgetStatsNetworkFailure : WidgetStatsResult()
+ data object WidgetStatsAPINotSupportedFailure : WidgetStatsResult()
data class WidgetStatsFailure(val errorMessage: String?) : WidgetStatsResult()
data class WidgetStats(
private val revenueModel: WCRevenueStatsModel?,
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/appwidgets/stats/today/UpdateTodayStatsWorker.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/appwidgets/stats/today/UpdateTodayStatsWorker.kt
index 7cbb4155aa8..b9544d5ca6e 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/appwidgets/stats/today/UpdateTodayStatsWorker.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/appwidgets/stats/today/UpdateTodayStatsWorker.kt
@@ -11,6 +11,13 @@ import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection
import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection.SelectionType
import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats
+import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats.WidgetStatsResult
+import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats.WidgetStatsResult.WidgetStats
+import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats.WidgetStatsResult.WidgetStatsAPINotSupportedFailure
+import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats.WidgetStatsResult.WidgetStatsAuthFailure
+import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats.WidgetStatsResult.WidgetStatsBatterySaverActive
+import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats.WidgetStatsResult.WidgetStatsFailure
+import com.woocommerce.android.ui.appwidgets.stats.GetWidgetStats.WidgetStatsResult.WidgetStatsNetworkFailure
import com.woocommerce.android.util.DateUtils
import com.woocommerce.android.util.locale.LocaleProvider
import dagger.assisted.Assisted
@@ -68,10 +75,19 @@ class UpdateTodayStatsWorker @AssistedInject constructor(
widgetId: Int,
remoteViews: RemoteViews,
site: SiteModel?,
- todayStatsResult: GetWidgetStats.WidgetStatsResult
+ todayStatsResult: WidgetStatsResult
): Result {
return when (todayStatsResult) {
- GetWidgetStats.WidgetStatsResult.WidgetStatsNetworkFailure -> {
+ WidgetStatsBatterySaverActive -> {
+ todayStatsWidgetUIHelper.displayError(
+ remoteViews = remoteViews,
+ errorMessageRes = R.string.stats_widget_battery_saver_error,
+ withRetryButton = false,
+ widgetId = widgetId
+ )
+ Result.failure()
+ }
+ WidgetStatsNetworkFailure -> {
todayStatsWidgetUIHelper.displayError(
remoteViews = remoteViews,
errorMessageRes = R.string.stats_widget_offline_error,
@@ -80,7 +96,7 @@ class UpdateTodayStatsWorker @AssistedInject constructor(
)
Result.retry()
}
- GetWidgetStats.WidgetStatsResult.WidgetStatsAPINotSupportedFailure -> {
+ WidgetStatsAPINotSupportedFailure -> {
todayStatsWidgetUIHelper.displayError(
remoteViews = remoteViews,
errorMessageRes = R.string.stats_widget_availability_message,
@@ -89,7 +105,7 @@ class UpdateTodayStatsWorker @AssistedInject constructor(
)
Result.failure()
}
- GetWidgetStats.WidgetStatsResult.WidgetStatsAuthFailure -> {
+ WidgetStatsAuthFailure -> {
todayStatsWidgetUIHelper.displayError(
remoteViews = remoteViews,
errorMessageRes = R.string.stats_widget_log_in_message,
@@ -98,7 +114,7 @@ class UpdateTodayStatsWorker @AssistedInject constructor(
)
Result.failure()
}
- is GetWidgetStats.WidgetStatsResult.WidgetStatsFailure -> {
+ is WidgetStatsFailure -> {
todayStatsWidgetUIHelper.displayError(
remoteViews = remoteViews,
errorMessageRes = R.string.stats_widget_error_no_data,
@@ -107,23 +123,32 @@ class UpdateTodayStatsWorker @AssistedInject constructor(
)
Result.retry()
}
- is GetWidgetStats.WidgetStatsResult.WidgetStats -> {
- if (site != null) {
- todayStatsWidgetUIHelper.displayInformation(
- stats = todayStatsResult,
- remoteViews = remoteViews
- )
- Result.success()
- } else {
- todayStatsWidgetUIHelper.displayError(
- remoteViews = remoteViews,
- errorMessageRes = R.string.stats_widget_error_no_data,
- withRetryButton = true,
- widgetId = widgetId
- )
- Result.retry()
- }
+ is WidgetStats -> {
+ handleStatsAvailable(todayStatsResult, site, remoteViews, widgetId)
}
}
}
+
+ private fun handleStatsAvailable(
+ todayStatsResult: WidgetStats,
+ site: SiteModel?,
+ remoteViews: RemoteViews,
+ widgetId: Int
+ ): Result {
+ if (site != null) {
+ todayStatsWidgetUIHelper.displayInformation(
+ stats = todayStatsResult,
+ remoteViews = remoteViews
+ )
+ return Result.success()
+ } else {
+ todayStatsWidgetUIHelper.displayError(
+ remoteViews = remoteViews,
+ errorMessageRes = R.string.stats_widget_error_no_data,
+ withRetryButton = true,
+ widgetId = widgetId
+ )
+ return Result.retry()
+ }
+ }
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeRepository.kt
index a5327c57c75..2d94bf3da21 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeRepository.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeRepository.kt
@@ -9,6 +9,7 @@ import com.woocommerce.android.media.MediaFilesRepository
import com.woocommerce.android.model.CreditCardType
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.products.details.ProductDetailRepository
+import com.woocommerce.android.util.FeatureFlag
import com.woocommerce.android.util.WooLog
import com.woocommerce.android.util.joinToUrl
import kotlinx.coroutines.flow.catch
@@ -39,13 +40,39 @@ class BlazeRepository @Inject constructor(
) {
companion object {
private const val BLAZE_CAMPAIGN_CREATION_ORIGIN = "wc-android"
+ const val CAMPAIGN_BUDGET_MODE_TOTAL = "total" // "total" for campaigns with defined end date
+ const val CAMPAIGN_BUDGET_MODE_DAILY = "daily" // "daily" for endless/evergreen campaigns
const val BLAZE_DEFAULT_CURRENCY_CODE = "USD" // For now only USD are supported
const val DEFAULT_CAMPAIGN_DURATION = 7 // Days
const val CAMPAIGN_MINIMUM_DAILY_SPEND = 5f // USD
const val CAMPAIGN_MAXIMUM_DAILY_SPEND = 50f // USD
const val CAMPAIGN_MAX_DURATION = 28 // Days
- const val DEFAULT_CAMPAIGN_BUDGET_MODE = "total" // "total" or "daily" for campaigns that run without end date
const val BLAZE_IMAGE_MINIMUM_SIZE_IN_PIXELS = 400 // Must be at least 400 x 400 pixels
+ const val WEEKLY_DURATION = 7 // Used to calculate weekly budget in endless campaigns
+ }
+
+ fun observeObjectives() = blazeCampaignsStore.observeBlazeCampaignObjectives().map {
+ it.map { objective ->
+ Objective(
+ objective.id,
+ objective.title,
+ objective.description,
+ objective.suitableForDescription
+ )
+ }
+ }
+
+ suspend fun fetchObjectives(): Result {
+ val result = blazeCampaignsStore.fetchBlazeCampaignObjectives(selectedSite.get())
+
+ return when {
+ result.isError -> {
+ WooLog.w(WooLog.T.BLAZE, "Failed to fetch objectives: ${result.error}")
+ Result.failure(OnChangedException(result.error))
+ }
+
+ else -> Result.success(Unit)
+ }
}
fun observeLanguages() = blazeCampaignsStore.observeBlazeTargetingLanguages()
@@ -142,10 +169,11 @@ class BlazeRepository @Inject constructor(
currencyCode = BLAZE_DEFAULT_CURRENCY_CODE,
durationInDays = DEFAULT_CAMPAIGN_DURATION,
startDate = Date().apply { time += 1.days.inWholeMilliseconds }, // By default start tomorrow
+ isEndlessCampaign = FeatureFlag.ENDLESS_CAMPAIGNS_SUPPORT.isEnabled()
)
val product = productDetailRepository.getProduct(productId)
- ?: productDetailRepository.fetchProductOrLoadFromCache(productId)!!
+ ?: productDetailRepository.fetchAndGetProduct(productId)!!
return CampaignDetails(
productId = productId,
@@ -269,8 +297,14 @@ class BlazeRepository @Inject constructor(
startDate = campaignDetails.budget.startDate,
endDate = campaignDetails.budget.endDate,
budget = BlazeCampaignCreationRequestBudget(
- mode = DEFAULT_CAMPAIGN_BUDGET_MODE,
- amount = campaignDetails.budget.totalBudget.toDouble(),
+ mode = when {
+ campaignDetails.budget.isEndlessCampaign -> CAMPAIGN_BUDGET_MODE_DAILY
+ else -> CAMPAIGN_BUDGET_MODE_TOTAL
+ },
+ amount = when {
+ campaignDetails.budget.isEndlessCampaign -> campaignDetails.budget.totalBudget / WEEKLY_DURATION
+ else -> campaignDetails.budget.totalBudget
+ }.toDouble(),
currency = BLAZE_DEFAULT_CURRENCY_CODE // To be replaced when more currencies are supported
),
targetUrl = campaignDetails.destinationParameters.targetUrl,
@@ -283,7 +317,8 @@ class BlazeRepository @Inject constructor(
devices = it.devices.map { device -> device.id },
topics = it.interests.map { interest -> interest.id }
)
- }
+ },
+ isEndlessCampaign = campaignDetails.budget.isEndlessCampaign
)
)
@@ -367,6 +402,14 @@ class BlazeRepository @Inject constructor(
data class RemoteImage(val mediaId: Long, override val uri: String) : BlazeCampaignImage
}
+ @Parcelize
+ data class Objective(
+ val id: String,
+ val title: String,
+ val description: String,
+ val suitableForDescription: String
+ ) : Parcelable
+
@Parcelize
data class TargetingParameters(
val locations: List = emptyList(),
@@ -397,6 +440,7 @@ class BlazeRepository @Inject constructor(
val currencyCode: String,
val durationInDays: Int,
val startDate: Date,
+ val isEndlessCampaign: Boolean
) : Parcelable {
val endDate: Date
get() = Date(startDate.time + durationInDays.days.inWholeMilliseconds)
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeUiMapper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeUiMapper.kt
new file mode 100644
index 00000000000..ff449ff3320
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeUiMapper.kt
@@ -0,0 +1,42 @@
+package com.woocommerce.android.ui.blaze
+
+import com.woocommerce.android.R
+import com.woocommerce.android.extensions.NumberExtensionsWrapper
+import com.woocommerce.android.util.CurrencyFormatter
+import org.wordpress.android.fluxc.model.blaze.BlazeCampaignModel
+
+fun BlazeCampaignModel.toUiState(
+ currencyFormatter: CurrencyFormatter,
+ numberExtensionsWrapper: NumberExtensionsWrapper
+) = BlazeCampaignUi(
+ product = BlazeProductUi(
+ name = title,
+ imgUrl = imageUrl.orEmpty(),
+ ),
+ status = CampaignStatusUi.fromString(uiStatus),
+ isEndlessCampaign = isEndlessCampaign,
+ impressions = numberExtensionsWrapper.compactNumberCompat(impressions),
+ clicks = numberExtensionsWrapper.compactNumberCompat(clicks),
+ formattedBudget = getBudgetValue(this, currencyFormatter),
+ budgetLabel = getBudgetTitle(this)
+)
+
+private fun getBudgetTitle(campaign: BlazeCampaignModel) =
+ when {
+ campaign.isEndlessCampaign -> R.string.blaze_campaign_status_budget_weekly
+ CampaignStatusUi.isActive(campaign.uiStatus) -> R.string.blaze_campaign_status_budget_remaining
+ else -> R.string.blaze_campaign_status_budget_total
+ }
+
+private fun getBudgetValue(campaign: BlazeCampaignModel, currencyFormatter: CurrencyFormatter): String =
+ currencyFormatter.formatCurrencyRounded(
+ when {
+ campaign.isEndlessCampaign -> getWeeklyBudget(campaign)
+ CampaignStatusUi.isActive(campaign.uiStatus) -> (campaign.totalBudget - campaign.spentBudget)
+ else -> campaign.totalBudget
+ },
+ BlazeRepository.BLAZE_DEFAULT_CURRENCY_CODE
+ )
+
+private fun getWeeklyBudget(campaign: BlazeCampaignModel): Double =
+ (campaign.totalBudget / campaign.durationInDays) * BlazeRepository.WEEKLY_DURATION
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeUiModels.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeUiModels.kt
index 5695e2e78dd..45dc6e6ee6a 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeUiModels.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeUiModels.kt
@@ -12,12 +12,11 @@ data class BlazeProductUi(
data class BlazeCampaignUi(
val product: BlazeProductUi,
val status: CampaignStatusUi?,
- val stats: List,
-)
-
-data class BlazeCampaignStat(
- @StringRes val name: Int,
- val value: String
+ val isEndlessCampaign: Boolean,
+ val impressions: String,
+ val clicks: String,
+ val formattedBudget: String,
+ @StringRes val budgetLabel: Int
)
enum class CampaignStatusUi(
@@ -74,5 +73,10 @@ enum class CampaignStatusUi(
else -> null
}
}
+
+ fun isActive(status: String): Boolean {
+ val campaignStatus = fromString(status)
+ return campaignStatus == Active || campaignStatus == Scheduled || campaignStatus == InModeration
+ }
}
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/IsBlazeEnabled.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/IsBlazeEnabled.kt
index 6ab18cb14a2..fe0f8a5c864 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/IsBlazeEnabled.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/IsBlazeEnabled.kt
@@ -1,17 +1,28 @@
package com.woocommerce.android.ui.blaze
import com.woocommerce.android.tools.SelectedSite
-import com.woocommerce.android.tools.SiteConnectionType
-import com.woocommerce.android.util.IsRemoteFeatureFlagEnabled
-import com.woocommerce.android.util.RemoteFeatureFlag.WOO_BLAZE
+import com.woocommerce.android.tools.SiteConnectionType.Jetpack
import javax.inject.Inject
class IsBlazeEnabled @Inject constructor(
- private val selectedSite: SelectedSite,
- private val isRemoteFeatureFlagEnabled: IsRemoteFeatureFlagEnabled,
+ private val selectedSite: SelectedSite
) {
- suspend operator fun invoke(): Boolean = selectedSite.getIfExists()?.isAdmin ?: false &&
- selectedSite.connectionType == SiteConnectionType.Jetpack &&
- selectedSite.getIfExists()?.canBlaze ?: false &&
- isRemoteFeatureFlagEnabled(WOO_BLAZE)
+ companion object {
+ private const val BLAZE_FOR_WOOCOMMERCE_PLUGIN_SLUG = "blaze-ads"
+ }
+
+ operator fun invoke(): Boolean = selectedSite.getIfExists()?.isAdmin ?: false &&
+ hasAValidJetpackConnectionForBlaze() &&
+ selectedSite.getIfExists()?.canBlaze ?: false
+
+ /**
+ * In order for Blaze to work, the site requires the Jetpack Sync module to be enabled. This means not all
+ * Jetpack connection will work. For now, Blaze will only be enabled for sites with Jetpack plugin installed and
+ * active, or for sites with Blaze for WooCommerce plugin installed and connected.
+ */
+ private fun hasAValidJetpackConnectionForBlaze() =
+ selectedSite.connectionType == Jetpack || isBlazeForWooCommercePluginActive()
+
+ private fun isBlazeForWooCommercePluginActive(): Boolean =
+ selectedSite.get().activeJetpackConnectionPlugins?.contains(BLAZE_FOR_WOOCOMMERCE_PLUGIN_SLUG) == true
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/campaigs/BlazeCampaignItem.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/campaigs/BlazeCampaignItem.kt
index f984f461c68..7543c13c145 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/campaigs/BlazeCampaignItem.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/campaigs/BlazeCampaignItem.kt
@@ -1,11 +1,14 @@
package com.woocommerce.android.ui.blaze.campaigs
+import android.content.res.Configuration
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
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.padding
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
@@ -13,10 +16,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.woocommerce.android.R
import com.woocommerce.android.ui.blaze.BlazeCampaignUi
@@ -65,13 +70,28 @@ fun BlazeCampaignItem(
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
)
- Row(modifier = Modifier.padding(top = dimensionResource(id = R.dimen.major_100))) {
- campaign.stats.forEach {
- CampaignStat(
- statName = stringResource(id = it.name),
- statValue = it.value
+ Row(
+ modifier = Modifier.padding(top = dimensionResource(id = R.dimen.major_100))
+ ) {
+ CampaignStat(
+ statName = stringResource(R.string.blaze_campaign_status_ctr_label),
+ statValue = stringResource(
+ id = R.string.blaze_campaign_status_ctr_value_shortened,
+ campaign.impressions,
+ campaign.clicks,
)
+ )
+ val orientation = LocalConfiguration.current.orientation
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ Spacer(modifier = Modifier.weight(1f))
+ } else {
+ Spacer(modifier = Modifier.width(16.dp))
}
+ CampaignStat(
+ modifier = Modifier.padding(start = 16.dp),
+ statName = stringResource(campaign.budgetLabel),
+ statValue = campaign.formattedBudget
+ )
}
}
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/campaigs/BlazeCampaignListScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/campaigs/BlazeCampaignListScreen.kt
index bd426606c19..62ebf6c0156 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/campaigs/BlazeCampaignListScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/campaigs/BlazeCampaignListScreen.kt
@@ -43,7 +43,6 @@ import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.woocommerce.android.R
-import com.woocommerce.android.ui.blaze.BlazeCampaignStat
import com.woocommerce.android.ui.blaze.BlazeCampaignUi
import com.woocommerce.android.ui.blaze.BlazeProductUi
import com.woocommerce.android.ui.blaze.CampaignStatusUi.Active
@@ -232,20 +231,11 @@ fun BlazeCampaignListScreenPreview() {
imgUrl = "https://picsum.photos/200/300",
),
status = Active,
- stats = listOf(
- BlazeCampaignStat(
- name = R.string.blaze_campaign_status_impressions,
- value = 100.toString()
- ),
- BlazeCampaignStat(
- name = R.string.blaze_campaign_status_clicks,
- value = 10.toString()
- ),
- BlazeCampaignStat(
- name = R.string.blaze_campaign_status_budget,
- value = 1000.toString()
- ),
- ),
+ isEndlessCampaign = false,
+ impressions = "6k",
+ clicks = "10",
+ formattedBudget = "$100",
+ budgetLabel = R.string.blaze_campaign_status_budget_total
),
onCampaignClicked = {}
)
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/campaigs/BlazeCampaignListViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/campaigs/BlazeCampaignListViewModel.kt
index fefb1569e3a..c0f99231dfa 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/campaigs/BlazeCampaignListViewModel.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/campaigs/BlazeCampaignListViewModel.kt
@@ -7,13 +7,12 @@ import com.woocommerce.android.R
import com.woocommerce.android.analytics.AnalyticsEvent.BLAZE_CAMPAIGN_DETAIL_SELECTED
import com.woocommerce.android.analytics.AnalyticsTracker
import com.woocommerce.android.analytics.AnalyticsTrackerWrapper
+import com.woocommerce.android.extensions.NumberExtensionsWrapper
import com.woocommerce.android.tools.SelectedSite
-import com.woocommerce.android.ui.blaze.BlazeCampaignStat
import com.woocommerce.android.ui.blaze.BlazeCampaignUi
-import com.woocommerce.android.ui.blaze.BlazeProductUi
import com.woocommerce.android.ui.blaze.BlazeUrlsHelper
import com.woocommerce.android.ui.blaze.BlazeUrlsHelper.BlazeFlowSource
-import com.woocommerce.android.ui.blaze.CampaignStatusUi
+import com.woocommerce.android.ui.blaze.toUiState
import com.woocommerce.android.util.CurrencyFormatter
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event
import com.woocommerce.android.viewmodel.ScopedViewModel
@@ -26,7 +25,6 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.withIndex
import kotlinx.coroutines.launch
-import org.wordpress.android.fluxc.persistence.blaze.BlazeCampaignsDao.BlazeCampaignEntity
import org.wordpress.android.fluxc.store.blaze.BlazeCampaignsStore
import javax.inject.Inject
@@ -39,7 +37,8 @@ class BlazeCampaignListViewModel @Inject constructor(
private val blazeUrlsHelper: BlazeUrlsHelper,
private val appPrefsWrapper: AppPrefsWrapper,
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper,
- private val currencyFormatter: CurrencyFormatter
+ private val currencyFormatter: CurrencyFormatter,
+ private val numberExtensionsWrapper: NumberExtensionsWrapper
) : ScopedViewModel(savedStateHandle) {
companion object {
private const val LOADING_TRANSITION_DELAY = 200L
@@ -64,7 +63,12 @@ class BlazeCampaignListViewModel @Inject constructor(
isCampaignCelebrationShown
) { campaigns, loadingMore, isBlazeCelebrationScreenShown ->
BlazeCampaignListState(
- campaigns = campaigns.map { mapToUiState(it) },
+ campaigns = campaigns.map {
+ ClickableCampaign(
+ campaignUi = it.toUiState(currencyFormatter, numberExtensionsWrapper),
+ onCampaignClicked = { onCampaignClicked(it.campaignId) }
+ )
+ },
onAddNewCampaignClicked = { onAddNewCampaignClicked() },
isLoading = loadingMore,
isCampaignCelebrationShown = isBlazeCelebrationScreenShown
@@ -75,6 +79,14 @@ class BlazeCampaignListViewModel @Inject constructor(
if (navArgs.isPostCampaignCreation) {
showCampaignCelebrationIfNeeded()
}
+ if (navArgs.campaignId != null) {
+ triggerEvent(
+ ShowCampaignDetails(
+ url = blazeUrlsHelper.buildCampaignDetailsUrl(navArgs.campaignId!!),
+ urlToTriggerExit = blazeUrlsHelper.buildCampaignsListUrl()
+ )
+ )
+ }
launch {
loadCampaigns(offset = 0)
}
@@ -105,45 +117,18 @@ class BlazeCampaignListViewModel @Inject constructor(
}
private fun onCampaignClicked(campaignId: String) {
- val url = blazeUrlsHelper.buildCampaignDetailsUrl(campaignId)
analyticsTrackerWrapper.track(
stat = BLAZE_CAMPAIGN_DETAIL_SELECTED,
properties = mapOf(AnalyticsTracker.KEY_BLAZE_SOURCE to BlazeFlowSource.CAMPAIGN_LIST.trackingName)
)
triggerEvent(
ShowCampaignDetails(
- url = url,
+ url = blazeUrlsHelper.buildCampaignDetailsUrl(campaignId),
urlToTriggerExit = blazeUrlsHelper.buildCampaignsListUrl()
)
)
}
- private fun mapToUiState(campaignEntity: BlazeCampaignEntity) =
- ClickableCampaign(
- campaignUi = BlazeCampaignUi(
- product = BlazeProductUi(
- name = campaignEntity.title,
- imgUrl = campaignEntity.imageUrl.orEmpty(),
- ),
- status = CampaignStatusUi.fromString(campaignEntity.uiStatus),
- stats = listOf(
- BlazeCampaignStat(
- name = R.string.blaze_campaign_status_impressions,
- value = campaignEntity.impressions.toString()
- ),
- BlazeCampaignStat(
- name = R.string.blaze_campaign_status_clicks,
- value = campaignEntity.clicks.toString()
- ),
- BlazeCampaignStat(
- name = R.string.blaze_campaign_status_budget,
- value = currencyFormatter.formatCurrencyRounded(campaignEntity.totalBudget)
- )
- )
- ),
- onCampaignClicked = { onCampaignClicked(campaignEntity.campaignId) }
- )
-
private fun onAddNewCampaignClicked() {
triggerEvent(LaunchBlazeCampaignCreation(BlazeFlowSource.CAMPAIGN_LIST))
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/BlazeCampaignCreationDispatcher.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/BlazeCampaignCreationDispatcher.kt
index 0ddcd99c364..817dc891260 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/BlazeCampaignCreationDispatcher.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/BlazeCampaignCreationDispatcher.kt
@@ -16,6 +16,7 @@ import com.woocommerce.android.ui.blaze.BlazeRepository
import com.woocommerce.android.ui.blaze.BlazeUrlsHelper.BlazeFlowSource
import com.woocommerce.android.ui.blaze.creation.intro.BlazeCampaignCreationIntroFragmentArgs
import com.woocommerce.android.ui.blaze.creation.preview.BlazeCampaignCreationPreviewFragmentArgs
+import com.woocommerce.android.ui.blaze.notification.AbandonedCampaignReminder
import com.woocommerce.android.ui.products.ProductStatus
import com.woocommerce.android.ui.products.list.ProductListRepository
import com.woocommerce.android.ui.products.selector.ProductSelectorFragment
@@ -35,6 +36,7 @@ import javax.inject.Inject
class BlazeCampaignCreationDispatcher @Inject constructor(
private val blazeRepository: BlazeRepository,
private val productListRepository: ProductListRepository,
+ private val abandonedCampaignReminder: AbandonedCampaignReminder,
private val coroutineDispatchers: CoroutineDispatchers,
private val analyticsTracker: AnalyticsTrackerWrapper,
) {
@@ -68,6 +70,7 @@ class BlazeCampaignCreationDispatcher @Inject constructor(
startCampaignCreationWithoutIntro(productId, source, handler)
}
}
+ abandonedCampaignReminder.scheduleReminderIfNeeded()
}
private suspend fun startCampaignCreationWithoutIntro(
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/BlazeCampaignCreationEditAdFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/BlazeCampaignCreationEditAdFragment.kt
index da5c5dfb273..feafc124a8a 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/BlazeCampaignCreationEditAdFragment.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/BlazeCampaignCreationEditAdFragment.kt
@@ -7,13 +7,17 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
+import com.woocommerce.android.extensions.handleResult
import com.woocommerce.android.extensions.navigateBackWithResult
+import com.woocommerce.android.extensions.navigateSafely
import com.woocommerce.android.mediapicker.MediaPickerHelper
import com.woocommerce.android.mediapicker.MediaPickerHelper.MediaPickerResultHandler
import com.woocommerce.android.model.Product.Image
import com.woocommerce.android.ui.base.BaseFragment
import com.woocommerce.android.ui.blaze.creation.ad.BlazeCampaignCreationEditAdViewModel.EditAdResult
import com.woocommerce.android.ui.blaze.creation.ad.BlazeCampaignCreationEditAdViewModel.ShowMediaLibrary
+import com.woocommerce.android.ui.blaze.creation.ad.BlazeCampaignCreationEditAdViewModel.ShowProductImagePicker
+import com.woocommerce.android.ui.blaze.creation.ad.ProductImagePickerViewModel.ImageSelectedResult
import com.woocommerce.android.ui.compose.composeView
import com.woocommerce.android.ui.main.AppBarStatus
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit
@@ -43,10 +47,12 @@ class BlazeCampaignCreationEditAdFragment : BaseFragment(), MediaPickerResultHan
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ handleResults()
viewModel.event.observe(viewLifecycleOwner) { event ->
when (event) {
is Exit -> findNavController().popBackStack()
is ShowMediaLibrary -> mediaPickerHelper.showMediaPicker(event.source)
+ is ShowProductImagePicker -> navigateToProductImagePicker(event.productId)
is ShowDialog -> event.showDialog()
is ExitWithResult<*> -> {
navigateBackWithResult(EDIT_AD_RESULT, event.data as EditAdResult)
@@ -55,6 +61,19 @@ class BlazeCampaignCreationEditAdFragment : BaseFragment(), MediaPickerResultHan
}
}
+ private fun navigateToProductImagePicker(productId: Long) {
+ findNavController().navigateSafely(
+ BlazeCampaignCreationEditAdFragmentDirections
+ .actionBlazeCampaignCreationEditAdFragmentToProductImagePickerFragment(productId)
+ )
+ }
+
+ private fun handleResults() {
+ handleResult(ProductImagePickerFragment.ON_PRODUCT_IMAGE_SELECTED) {
+ viewModel.onWPMediaSelected(it.productImage)
+ }
+ }
+
override fun onDeviceMediaSelected(imageUris: List, source: String) {
if (imageUris.isNotEmpty()) {
viewModel.onLocalImageSelected(imageUris.first().toString())
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/BlazeCampaignCreationEditAdScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/BlazeCampaignCreationEditAdScreen.kt
index b3fdd044dd5..f880be2d77a 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/BlazeCampaignCreationEditAdScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/BlazeCampaignCreationEditAdScreen.kt
@@ -69,6 +69,7 @@ fun BlazeCampaignCreationPreviewScreen(viewModel: BlazeCampaignCreationEditAdVie
onNextSuggestionTapped = viewModel::onNextSuggestionTapped,
onBackButtonTapped = viewModel::onBackButtonTapped,
onMediaPickerDialogDismissed = viewModel::onMediaPickerDialogDismissed,
+ onProductImagesRequested = viewModel::onProductImagesRequested,
onMediaLibraryRequested = viewModel::onMediaLibraryRequested,
onSaveTapped = viewModel::onSaveTapped
)
@@ -85,13 +86,16 @@ private fun BlazeCampaignCreationEditAdScreen(
onNextSuggestionTapped: () -> Unit,
onBackButtonTapped: () -> Unit,
onMediaPickerDialogDismissed: () -> Unit,
+ onProductImagesRequested: () -> Unit,
onMediaLibraryRequested: (DataSource) -> Unit,
onSaveTapped: () -> Unit
) {
if (viewState.isMediaPickerDialogVisible) {
MediaPickerDialog(
- onMediaPickerDialogDismissed,
- onMediaLibraryRequested
+ onDismissRequest = onMediaPickerDialogDismissed,
+ onMediaLibraryRequested = onMediaLibraryRequested,
+ withProductImagePicker = true,
+ onProductImagesRequested = onProductImagesRequested
)
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/BlazeCampaignCreationEditAdViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/BlazeCampaignCreationEditAdViewModel.kt
index e61e33d138e..ca7fcfe6396 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/BlazeCampaignCreationEditAdViewModel.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/BlazeCampaignCreationEditAdViewModel.kt
@@ -170,7 +170,13 @@ class BlazeCampaignCreationEditAdViewModel @Inject constructor(
}
}
+ fun onProductImagesRequested() {
+ triggerEvent(ShowProductImagePicker(navArgs.productId))
+ setMediaPickerDialogVisibility(false)
+ }
+
data class ShowMediaLibrary(val source: MediaPickerSetup.DataSource) : Event()
+ data class ShowProductImagePicker(val productId: Long) : Event()
@Parcelize
data class ViewState(
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/ProductImagePickerFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/ProductImagePickerFragment.kt
new file mode 100644
index 00000000000..fe204073a8a
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/ProductImagePickerFragment.kt
@@ -0,0 +1,45 @@
+package com.woocommerce.android.ui.blaze.creation.ad
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import com.woocommerce.android.extensions.navigateBackWithResult
+import com.woocommerce.android.ui.base.BaseFragment
+import com.woocommerce.android.ui.blaze.creation.ad.ProductImagePickerViewModel.ImageSelectedResult
+import com.woocommerce.android.ui.compose.composeView
+import com.woocommerce.android.ui.main.AppBarStatus
+import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit
+import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ExitWithResult
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class ProductImagePickerFragment : BaseFragment() {
+ companion object {
+ const val ON_PRODUCT_IMAGE_SELECTED = "on_product_image_selected"
+ }
+
+ override val activityAppBarStatus: AppBarStatus
+ get() = AppBarStatus.Hidden
+
+ val viewModel: ProductImagePickerViewModel by viewModels()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ return composeView {
+ ProductImagePickerScreen(viewModel = viewModel)
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ viewModel.event.observe(viewLifecycleOwner) { event ->
+ when (event) {
+ is Exit -> findNavController().popBackStack()
+ is ExitWithResult<*> -> {
+ navigateBackWithResult(ON_PRODUCT_IMAGE_SELECTED, event.data as ImageSelectedResult)
+ }
+ }
+ }
+ }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/ProductImagePickerViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/ProductImagePickerViewModel.kt
new file mode 100644
index 00000000000..572a77d651a
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/ProductImagePickerViewModel.kt
@@ -0,0 +1,61 @@
+package com.woocommerce.android.ui.blaze.creation.ad
+
+import android.os.Parcelable
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.asLiveData
+import androidx.lifecycle.viewModelScope
+import com.woocommerce.android.model.Product
+import com.woocommerce.android.ui.products.details.ProductDetailRepository
+import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit
+import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ExitWithResult
+import com.woocommerce.android.viewmodel.ScopedViewModel
+import com.woocommerce.android.viewmodel.getStateFlow
+import com.woocommerce.android.viewmodel.navArgs
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.parcelize.Parcelize
+import javax.inject.Inject
+
+@HiltViewModel
+class ProductImagePickerViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val productRepository: ProductDetailRepository,
+) : ScopedViewModel(savedStateHandle) {
+ private val navArgs: ProductImagePickerFragmentArgs by savedStateHandle.navArgs()
+
+ private val _viewState = savedStateHandle.getStateFlow(
+ scope = viewModelScope,
+ initialValue = ViewState(emptyList())
+ )
+ val viewState = _viewState.asLiveData()
+
+ init {
+ launch {
+ val product = productRepository.getProduct(navArgs.productId)
+ product?.let {
+ _viewState.update { it.copy(productImages = product.images) }
+ }
+ }
+ }
+
+ fun onImageSelected(productImage: Product.Image) {
+ triggerEvent(
+ ExitWithResult(
+ ImageSelectedResult(productImage = productImage)
+ )
+ )
+ }
+
+ fun onBackButtonTapped() {
+ triggerEvent(Exit)
+ }
+
+ @Parcelize
+ data class ViewState(
+ val productImages: List
+ ) : Parcelable
+
+ @Parcelize
+ data class ImageSelectedResult(val productImage: Product.Image) : Parcelable
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/ProductImageScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/ProductImageScreen.kt
new file mode 100644
index 00000000000..cc0773cf5b0
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/ad/ProductImageScreen.kt
@@ -0,0 +1,121 @@
+package com.woocommerce.android.ui.blaze.creation.ad
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells.Adaptive
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import coil.request.ImageRequest.Builder
+import com.woocommerce.android.R
+import com.woocommerce.android.model.Product
+import com.woocommerce.android.ui.compose.component.Toolbar
+
+@Composable
+fun ProductImagePickerScreen(viewModel: ProductImagePickerViewModel) {
+ viewModel.viewState.observeAsState().value?.let { viewState ->
+ ProductImagePickerScreen(
+ viewState = viewState,
+ onImageSelected = viewModel::onImageSelected,
+ onBackButtonTapped = viewModel::onBackButtonTapped
+ )
+ }
+}
+
+@Composable
+fun ProductImagePickerScreen(
+ viewState: ProductImagePickerViewModel.ViewState,
+ onImageSelected: (Product.Image) -> Unit,
+ onBackButtonTapped: () -> Unit
+) {
+ Scaffold(
+ topBar = {
+ Toolbar(
+ title = stringResource(id = R.string.blaze_campaign_product_photo_picker_title),
+ onNavigationButtonClick = onBackButtonTapped,
+ navigationIcon = Icons.AutoMirrored.Filled.ArrowBack,
+ )
+ },
+ backgroundColor = MaterialTheme.colors.surface
+ ) { paddingValues ->
+ when {
+ viewState.productImages.isEmpty() -> ProductPhotosEmpty()
+
+ else -> ProductImageGrid(
+ viewState = viewState,
+ onImageSelected = onImageSelected,
+ modifier = Modifier.padding(paddingValues)
+ )
+ }
+ }
+}
+
+@Composable
+private fun ProductPhotosEmpty() {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = stringResource(id = R.string.blaze_campaign_product_photo_picker_empty),
+ style = MaterialTheme.typography.h6,
+ fontWeight = FontWeight.Bold
+ )
+ }
+}
+
+@Composable
+private fun ProductImageGrid(
+ viewState: ProductImagePickerViewModel.ViewState,
+ onImageSelected: (Product.Image) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ LazyVerticalGrid(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ columns = Adaptive(minSize = 128.dp),
+ verticalArrangement = Arrangement.spacedBy(3.dp),
+ horizontalArrangement = Arrangement.spacedBy(3.dp)
+ ) {
+ items(viewState.productImages) { photoUrl ->
+ AsyncImage(
+ model = Builder(LocalContext.current)
+ .data(photoUrl.source)
+ .crossfade(true)
+ .build(),
+ fallback = painterResource(R.drawable.blaze_campaign_product_placeholder),
+ placeholder = painterResource(R.drawable.blaze_campaign_product_placeholder),
+ error = painterResource(R.drawable.blaze_campaign_product_placeholder),
+ contentDescription = stringResource(
+ id = R.string.blaze_campaign_product_photo_picker_photo_content_description
+ ),
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(128.dp)
+ .clickable { onImageSelected(photoUrl) }
+ )
+ }
+ }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/budget/BlazeCampaignBudgetScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/budget/BlazeCampaignBudgetScreen.kt
index dddb01891c5..b97bc64a505 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/budget/BlazeCampaignBudgetScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/budget/BlazeCampaignBudgetScreen.kt
@@ -1,7 +1,9 @@
package com.woocommerce.android.ui.blaze.creation.budget
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -9,10 +11,12 @@ 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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.InlineTextContent
+import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
@@ -28,6 +32,7 @@ import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -39,6 +44,9 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.Placeholder
+import androidx.compose.ui.text.PlaceholderVerticalAlign
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -47,15 +55,23 @@ import com.woocommerce.android.R
import com.woocommerce.android.R.color
import com.woocommerce.android.R.dimen
import com.woocommerce.android.R.drawable
-import com.woocommerce.android.extensions.formatToMMMddYYYY
+import com.woocommerce.android.extensions.formatToLocalizedMedium
+import com.woocommerce.android.ui.blaze.BlazeRepository.Companion.CAMPAIGN_MAXIMUM_DAILY_SPEND
+import com.woocommerce.android.ui.blaze.BlazeRepository.Companion.CAMPAIGN_MINIMUM_DAILY_SPEND
+import com.woocommerce.android.ui.blaze.creation.budget.BlazeCampaignBudgetViewModel.BudgetUiState
import com.woocommerce.android.ui.blaze.creation.budget.BlazeCampaignBudgetViewModel.Companion.MAX_DATE_LIMIT_IN_DAYS
+import com.woocommerce.android.ui.compose.animations.SkeletonView
import com.woocommerce.android.ui.compose.component.BottomSheetHandle
+import com.woocommerce.android.ui.compose.component.BottomSheetSwitchColors
import com.woocommerce.android.ui.compose.component.DatePickerDialog
import com.woocommerce.android.ui.compose.component.Toolbar
import com.woocommerce.android.ui.compose.component.WCColoredButton
import com.woocommerce.android.ui.compose.component.WCModalBottomSheetLayout
+import com.woocommerce.android.ui.compose.component.WCSwitch
import com.woocommerce.android.ui.compose.component.WCTextButton
import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews
+import com.woocommerce.android.util.FeatureFlag
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.Calendar
import java.util.Date
@@ -68,11 +84,12 @@ fun CampaignBudgetScreen(viewModel: BlazeCampaignBudgetViewModel) {
onBackPressed = viewModel::onBackPressed,
onEditDurationTapped = viewModel::onEditDurationTapped,
onImpressionsInfoTapped = viewModel::onImpressionsInfoTapped,
- onBudgetUpdated = viewModel::onBudgetUpdated,
+ onBudgetUpdated = viewModel::onDailyBudgetUpdated,
onStartDateChanged = viewModel::onStartDateChanged,
onBudgetChangeFinished = viewModel::onBudgetChangeFinished,
onUpdateTapped = viewModel::onUpdateTapped,
- onApplyDurationTapped = viewModel::onApplyDurationTapped
+ onApplyDurationTapped = viewModel::onApplyDurationTapped,
+ onDurationSliderUpdated = viewModel::onDurationSliderUpdated
)
}
}
@@ -80,7 +97,7 @@ fun CampaignBudgetScreen(viewModel: BlazeCampaignBudgetViewModel) {
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun CampaignBudgetScreen(
- state: BlazeCampaignBudgetViewModel.BudgetUiState,
+ state: BudgetUiState,
onBackPressed: () -> Unit,
onEditDurationTapped: () -> Unit,
onImpressionsInfoTapped: () -> Unit,
@@ -88,7 +105,8 @@ private fun CampaignBudgetScreen(
onStartDateChanged: (Long) -> Unit,
onBudgetChangeFinished: () -> Unit,
onUpdateTapped: () -> Unit,
- onApplyDurationTapped: (Int) -> Unit
+ onApplyDurationTapped: (Int, Boolean, Long) -> Unit,
+ onDurationSliderUpdated: (Int, Long) -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
val modalSheetState = rememberModalBottomSheetState(
@@ -100,7 +118,6 @@ private fun CampaignBudgetScreen(
Scaffold(
topBar = {
Toolbar(
- title = stringResource(id = R.string.blaze_campaign_budget_toolbar_title),
onNavigationButtonClick = onBackPressed,
navigationIcon = Icons.AutoMirrored.Filled.ArrowBack
)
@@ -121,9 +138,13 @@ private fun CampaignBudgetScreen(
state.showCampaignDurationBottomSheet -> EditDurationBottomSheet(
budgetUiState = state,
onStartDateChanged = { onStartDateChanged(it) },
- onApplyTapped = {
- onApplyDurationTapped(it)
+ onApplyTapped = { duration, isEndlessCampaign, startDate ->
+ onApplyDurationTapped(duration, isEndlessCampaign, startDate)
coroutineScope.launch { modalSheetState.hide() }
+ },
+ onCancelTapped = { coroutineScope.launch { modalSheetState.hide() } },
+ onDurationSliderUpdated = { duration, startDate ->
+ onDurationSliderUpdated(duration, startDate)
}
)
}
@@ -143,14 +164,16 @@ private fun CampaignBudgetScreen(
},
onBudgetUpdated = onBudgetUpdated,
onBudgetChangeFinished = onBudgetChangeFinished,
- modifier = Modifier.weight(1f)
- )
- EditDurationSection(
- campaignDurationDates = state.campaignDurationDates,
onEditDurationTapped = {
onEditDurationTapped()
coroutineScope.launch { modalSheetState.show() }
},
+ modifier = Modifier.weight(1f)
+ )
+ CampaignBudgetFooter(
+ isEndlessCampaign = state.isEndlessCampaign,
+ formattedBudget = state.formattedTotalBudget,
+ durationInDays = state.durationInDays,
onUpdateTapped = onUpdateTapped
)
}
@@ -160,77 +183,116 @@ private fun CampaignBudgetScreen(
@Composable
private fun EditBudgetSection(
- state: BlazeCampaignBudgetViewModel.BudgetUiState,
+ state: BudgetUiState,
onBudgetUpdated: (Float) -> Unit,
onImpressionsInfoTapped: () -> Unit,
onBudgetChangeFinished: () -> Unit,
+ onEditDurationTapped: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
- .padding(start = 28.dp, end = 28.dp)
+ .padding(start = 16.dp, end = 16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
- modifier = Modifier.padding(
- top = 40.dp,
- bottom = 90.dp
- ),
- text = stringResource(id = R.string.blaze_campaign_budget_subtitle),
+ modifier = Modifier.padding(16.dp),
+ text = stringResource(id = R.string.blaze_campaign_budget_toolbar_title),
+ style = MaterialTheme.typography.h4,
+ fontWeight = FontWeight.Bold,
+ )
+ Text(
+ modifier = Modifier.padding(bottom = 64.dp),
+ text = stringResource(id = R.string.blaze_campaign_budget_duration_subtitle),
style = MaterialTheme.typography.subtitle1,
textAlign = TextAlign.Center,
- lineHeight = 24.sp,
color = colorResource(id = color.color_on_surface_medium)
)
Text(
modifier = Modifier.padding(bottom = 8.dp),
- text = stringResource(id = R.string.blaze_campaign_budget_total_spend),
+ text = stringResource(id = R.string.blaze_campaign_budget_daily_spend_label),
style = MaterialTheme.typography.body1,
color = colorResource(id = color.color_on_surface_medium)
)
Text(
- text = " $${state.totalBudget.toInt()} USD",
+ text = state.formattedDailySpending,
style = MaterialTheme.typography.h4,
fontWeight = FontWeight.Bold,
)
- Text(
- text = stringResource(id = R.string.blaze_campaign_budget_days_duration, state.durationInDays),
- style = MaterialTheme.typography.h4,
- color = colorResource(id = color.color_on_surface_medium)
- )
- Text(
- modifier = Modifier.padding(top = 40.dp),
- text = stringResource(id = R.string.blaze_campaign_budget_daily_spend, state.dailySpending),
- color = colorResource(id = color.color_on_surface_medium),
- style = MaterialTheme.typography.subtitle1,
- )
Slider(
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp),
- value = state.totalBudget,
- valueRange = state.budgetRangeMin..state.budgetRangeMax,
+ value = state.dailySpend,
+ valueRange = CAMPAIGN_MINIMUM_DAILY_SPEND..CAMPAIGN_MAXIMUM_DAILY_SPEND,
onValueChange = { onBudgetUpdated(it) },
onValueChangeFinished = { onBudgetChangeFinished() },
colors = SliderDefaults.colors(
inactiveTrackColor = colorResource(id = color.divider_color)
)
)
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(text = stringResource(id = R.string.blaze_campaign_budget_reach_forecast))
- Icon(
- modifier = Modifier
- .padding(start = 4.dp)
- .clickable { onImpressionsInfoTapped() },
- painter = painterResource(id = drawable.ic_info_outline_20dp),
- contentDescription = null
+ CampaignDurationRow(
+ formattedStartDate = state.formattedStartDate,
+ formattedEndDate = state.formattedEndDate,
+ isEndlessCampaign = state.isEndlessCampaign,
+ onEditDurationTapped = onEditDurationTapped,
+ modifier = Modifier.padding(top = 24.dp)
+ )
+ CampaignImpressionsRow(
+ state = state,
+ onImpressionsInfoTapped = onImpressionsInfoTapped,
+ onBudgetChangeFinished = onBudgetChangeFinished,
+ modifier = Modifier.padding(top = 18.dp)
+ )
+ }
+}
+
+@Composable
+private fun CampaignImpressionsRow(
+ state: BudgetUiState,
+ onImpressionsInfoTapped: () -> Unit,
+ onBudgetChangeFinished: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(modifier = modifier) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onImpressionsInfoTapped() }
+ .padding(top = 8.dp, bottom = 8.dp, end = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ val id = "inlineIcon"
+ val text = buildAnnotatedString {
+ append(stringResource(id = R.string.blaze_campaign_budget_reach_forecast))
+ appendInlineContent(id = id, alternateText = "[Icon]")
+ }
+ val inlineContent = mapOf(
+ id to InlineTextContent(
+ Placeholder(
+ width = 24.sp,
+ height = 20.sp,
+ placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter
+ )
+ ) {
+ Icon(
+ modifier = Modifier.padding(start = 4.dp),
+ painter = painterResource(id = drawable.ic_info_outline_20dp),
+ contentDescription = ""
+ )
+ }
+ )
+ Text(
+ text = text,
+ inlineContent = inlineContent,
+ style = MaterialTheme.typography.body1,
+ color = colorResource(id = color.color_on_surface_medium)
)
}
- Spacer(modifier = Modifier.height(6.dp))
if (state.forecast.isLoading) {
- CircularProgressIndicator(
+ SkeletonView(
modifier = Modifier
- .align(Alignment.CenterHorizontally)
- .size(20.dp),
+ .size(height = 20.dp, width = 140.dp)
+ .padding(top = 8.dp)
)
} else {
if (state.forecast.isError) {
@@ -241,57 +303,111 @@ private fun EditBudgetSection(
)
} else {
Text(
- text = "${state.forecast.impressionsMin} - ${state.forecast.impressionsMax}",
+ modifier = Modifier.padding(top = 6.dp),
+ text = "${state.forecast.formattedImpressionsMin} - ${state.forecast.formattedImpressionsMax}",
+ style = MaterialTheme.typography.h6,
fontWeight = FontWeight.SemiBold,
- lineHeight = 24.sp,
)
}
}
- Spacer(modifier = Modifier.height(24.dp))
}
}
@Composable
-private fun EditDurationSection(
- campaignDurationDates: String,
+private fun CampaignDurationRow(
+ formattedStartDate: String,
+ formattedEndDate: String,
+ isEndlessCampaign: Boolean,
onEditDurationTapped: () -> Unit,
- onUpdateTapped: () -> Unit,
+ modifier: Modifier = Modifier
) {
- Column {
- Divider()
- Column(
- modifier = Modifier.padding(
- start = 16.dp,
- end = 16.dp,
- top = 16.dp,
- bottom = 24.dp
- )
+ Column(modifier = modifier) {
+ Text(
+ text = stringResource(id = R.string.blaze_campaign_budget_scheduled_section_title),
+ style = MaterialTheme.typography.body1,
+ color = colorResource(id = color.color_on_surface_medium)
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
) {
- Text(
- text = stringResource(id = R.string.blaze_campaign_budget_duration_section_title),
- style = MaterialTheme.typography.body1,
- color = colorResource(id = color.color_on_surface_medium)
- )
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
+ when {
+ isEndlessCampaign ->
+ Text(
+ text = stringResource(
+ id = R.string.blaze_campaign_budget_duration_endless_campaign_value,
+ formattedStartDate
+ ),
+ style = MaterialTheme.typography.h6,
+ fontWeight = FontWeight.SemiBold,
+ )
+
+ else -> {
+ Text(
+ text = "$formattedStartDate - $formattedEndDate",
+ style = MaterialTheme.typography.h6,
+ fontWeight = FontWeight.SemiBold,
+ )
+ }
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ WCTextButton(onClick = onEditDurationTapped) {
Text(
- text = campaignDurationDates,
- style = MaterialTheme.typography.subtitle2,
+ text = stringResource(id = R.string.blaze_campaign_budget_edit_duration_button),
+ style = MaterialTheme.typography.h6,
fontWeight = FontWeight.SemiBold,
)
- WCTextButton(
- onClick = onEditDurationTapped
- ) {
- Text(text = stringResource(id = R.string.blaze_campaign_budget_edit_duration_button))
- }
}
- WCColoredButton(
- modifier = Modifier.fillMaxWidth(),
- onClick = onUpdateTapped,
- text = stringResource(id = R.string.blaze_campaign_budget_update_button)
+ }
+ }
+}
+
+@Composable
+private fun CampaignBudgetFooter(
+ isEndlessCampaign: Boolean,
+ formattedBudget: String,
+ durationInDays: Int,
+ onUpdateTapped: () -> Unit
+) {
+ Column {
+ Divider()
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = formattedBudget,
+ style = MaterialTheme.typography.subtitle1,
+ fontWeight = FontWeight.SemiBold,
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = if (isEndlessCampaign) {
+ stringResource(id = R.string.blaze_campaign_budget_footer_weekly_spend)
+ } else {
+ stringResource(
+ id = R.string.blaze_campaign_budget_days_duration,
+ durationInDays
+ )
+ },
+ style = MaterialTheme.typography.body1,
+ color = colorResource(id = color.color_on_surface_medium),
+ fontWeight = FontWeight.SemiBold,
)
}
+ WCColoredButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ start = 16.dp,
+ end = 16.dp,
+ bottom = 24.dp
+ ),
+ onClick = onUpdateTapped,
+ text = stringResource(id = R.string.blaze_campaign_budget_update_button)
+ )
}
}
@@ -316,7 +432,10 @@ private fun ImpressionsInfoBottomSheet(
WCTextButton(
onClick = onDoneTapped
) {
- Text(text = stringResource(id = R.string.blaze_campaign_budget_impressions_done_button))
+ Text(
+ style = MaterialTheme.typography.h6,
+ text = stringResource(id = R.string.blaze_campaign_budget_impressions_done_button)
+ )
}
}
Divider()
@@ -331,88 +450,164 @@ private fun ImpressionsInfoBottomSheet(
@Composable
private fun EditDurationBottomSheet(
- budgetUiState: BlazeCampaignBudgetViewModel.BudgetUiState,
+ budgetUiState: BudgetUiState,
onStartDateChanged: (Long) -> Unit,
- onApplyTapped: (Int) -> Unit,
+ onApplyTapped: (Int, Boolean, Long) -> Unit,
+ onCancelTapped: () -> Unit,
+ onDurationSliderUpdated: (Int, Long) -> Unit,
modifier: Modifier = Modifier,
) {
+ val coroutineScope = rememberCoroutineScope()
var showDatePicker by remember { mutableStateOf(false) }
+ var selectedStartDate by remember { mutableStateOf(Date(budgetUiState.confirmedCampaignStartDateMillis)) }
+ var isEndlessCampaign by remember { mutableStateOf(budgetUiState.isEndlessCampaign) }
+ var sliderPosition by remember { mutableFloatStateOf(budgetUiState.durationInDays.toFloat()) }
+
if (showDatePicker) {
DatePickerDialog(
- currentDate = Date(budgetUiState.confirmedCampaignStartDateMillis),
+ currentDate = selectedStartDate,
minDate = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, 1) }.time,
maxDate = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, MAX_DATE_LIMIT_IN_DAYS) }.time,
onDateSelected = {
onStartDateChanged(it.time)
+ selectedStartDate = it
showDatePicker = false
},
onDismissRequest = { showDatePicker = false }
)
}
- var sliderPosition by remember { mutableStateOf(budgetUiState.durationInDays.toFloat()) }
Column(
modifier = modifier
- .padding(16.dp)
+ .padding(vertical = 16.dp)
.verticalScroll(rememberScrollState())
) {
- Text(
- text = stringResource(id = R.string.blaze_campaign_budget_duration_bottom_sheet_title),
- style = MaterialTheme.typography.h6,
- fontWeight = FontWeight.SemiBold,
- )
- Text(
- modifier = Modifier
- .padding(top = 40.dp)
- .fillMaxWidth(),
- text = stringResource(
- id = R.string.blaze_campaign_budget_duration_bottom_sheet_duration,
- sliderPosition.toInt()
- ),
- style = MaterialTheme.typography.subtitle1,
- fontWeight = FontWeight.SemiBold,
- textAlign = TextAlign.Center
- )
- Slider(
- modifier = Modifier
- .padding(top = 8.dp, bottom = 8.dp)
- .fillMaxWidth(),
- value = sliderPosition,
- valueRange = budgetUiState.durationRangeMin..budgetUiState.durationRangeMax,
- onValueChange = { sliderPosition = it },
- colors = SliderDefaults.colors(
- inactiveTrackColor = colorResource(id = color.divider_color)
- )
- )
Row(
- modifier = Modifier.padding(top = 40.dp),
+ modifier = Modifier.padding(
+ start = 16.dp,
+ end = 16.dp,
+ bottom = 8.dp
+ ),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f),
- text = stringResource(id = R.string.blaze_campaign_budget_duration_bottom_sheet_starts),
- style = MaterialTheme.typography.body1,
+ text = stringResource(id = R.string.blaze_campaign_budget_scheduled_section_title),
+ style = MaterialTheme.typography.h6,
)
- Text(
+ WCTextButton(
+ onClick = {
+ sliderPosition = budgetUiState.durationInDays.toFloat()
+ isEndlessCampaign = budgetUiState.isEndlessCampaign
+ selectedStartDate = Date(budgetUiState.confirmedCampaignStartDateMillis)
+ coroutineScope.launch {
+ @Suppress("MagicNumber")
+ delay(400)
+ onCancelTapped()
+ }
+ }
+ ) {
+ Text(
+ text = stringResource(id = R.string.blaze_campaign_budget_duration_cancel_button),
+ style = MaterialTheme.typography.h6,
+ )
+ }
+ }
+ Divider()
+ Column(modifier = Modifier.padding(horizontal = 16.dp)) {
+ Row(
+ modifier = Modifier.padding(top = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = stringResource(id = R.string.blaze_campaign_budget_duration_bottom_sheet_start_date),
+ style = MaterialTheme.typography.body1,
+ )
+ Text(
+ modifier = Modifier
+ .clickable { showDatePicker = !showDatePicker }
+ .clip(RoundedCornerShape(4.dp))
+ .background(colorResource(id = color.divider_color))
+ .padding(8.dp),
+ text = selectedStartDate.formatToLocalizedMedium(),
+ style = MaterialTheme.typography.body1,
+ )
+ }
+ if (FeatureFlag.ENDLESS_CAMPAIGNS_SUPPORT.isEnabled()) {
+ WCSwitch(
+ text = stringResource(id = R.string.blaze_campaign_budget_duration_endless_switch_label),
+ checked = !isEndlessCampaign,
+ onCheckedChange = { isEndlessCampaign = !it },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp),
+ colors = BottomSheetSwitchColors()
+ )
+ AnimatedVisibility(isEndlessCampaign) {
+ Text(
+ modifier = Modifier.padding(top = 10.dp),
+ text = stringResource(id = R.string.blaze_campaign_budget_duration_endless_description),
+ style = MaterialTheme.typography.body1,
+ color = colorResource(id = color.color_on_surface_medium)
+ )
+ }
+ }
+ AnimatedVisibility(isEndlessCampaign.not()) {
+ Column(modifier = Modifier.padding(top = 16.dp)) {
+ Row(modifier = Modifier.fillMaxWidth()) {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = stringResource(id = R.string.blaze_campaign_budget_duration_current_duration),
+ style = MaterialTheme.typography.body1,
+ )
+ Text(
+ text = stringResource(
+ id = R.string.blaze_campaign_budget_duration_bottom_sheet_duration,
+ sliderPosition.toInt()
+ ),
+ style = MaterialTheme.typography.subtitle1,
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center
+ )
+ Text(
+ modifier = Modifier.padding(start = 6.dp),
+ text = stringResource(
+ id = R.string.blaze_campaign_budget_duration_bottom_sheet_end_date,
+ budgetUiState.formattedEndDate
+ ),
+ style = MaterialTheme.typography.body1,
+ )
+ }
+ Slider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp),
+ value = sliderPosition,
+ valueRange = budgetUiState.durationRangeMin..budgetUiState.durationRangeMax,
+ onValueChange = {
+ sliderPosition = it
+ onDurationSliderUpdated(it.toInt(), selectedStartDate.time)
+ },
+ colors = SliderDefaults.colors(
+ inactiveTrackColor = colorResource(id = color.divider_color)
+ )
+ )
+ }
+ }
+ WCColoredButton(
modifier = Modifier
- .clickable { showDatePicker = !showDatePicker }
- .clip(RoundedCornerShape(4.dp))
- .background(colorResource(id = color.divider_color))
- .padding(8.dp),
- text = Date(budgetUiState.bottomSheetCampaignStartDateMillis).formatToMMMddYYYY(),
- style = MaterialTheme.typography.body1,
+ .padding(
+ top = 30.dp,
+ bottom = 16.dp
+ )
+ .fillMaxWidth(),
+ onClick = {
+ onApplyTapped(sliderPosition.toInt(), isEndlessCampaign, selectedStartDate.time)
+ },
+ text = stringResource(id = R.string.blaze_campaign_budget_duration_bottom_sheet_apply_button)
)
}
- WCColoredButton(
- modifier = Modifier
- .padding(
- top = 30.dp,
- bottom = 16.dp
- )
- .fillMaxWidth(),
- onClick = { onApplyTapped(sliderPosition.toInt()) },
- text = stringResource(id = R.string.blaze_campaign_budget_duration_bottom_sheet_apply_button)
- )
}
}
@@ -420,26 +615,27 @@ private fun EditDurationBottomSheet(
@Composable
private fun CampaignBudgetScreenPreview() {
CampaignBudgetScreen(
- state = BlazeCampaignBudgetViewModel.BudgetUiState(
+ state = BudgetUiState(
currencyCode = "USD",
totalBudget = 35f,
- budgetRangeMin = 5f,
- budgetRangeMax = 35f,
- dailySpending = "$5",
+ formattedTotalBudget = "$35",
+ dailySpend = 5f,
+ formattedDailySpending = "$5",
durationInDays = 7,
durationRangeMin = 1f,
durationRangeMax = 28f,
forecast = BlazeCampaignBudgetViewModel.ForecastUi(
isLoading = false,
- impressionsMin = 0,
- impressionsMax = 0,
+ formattedImpressionsMin = "0",
+ formattedImpressionsMax = "0",
isError = false
),
confirmedCampaignStartDateMillis = Date().time,
- bottomSheetCampaignStartDateMillis = Date().time,
- campaignDurationDates = "Dec 13 - Dec 20, 2023",
showImpressionsBottomSheet = false,
- showCampaignDurationBottomSheet = false
+ showCampaignDurationBottomSheet = false,
+ isEndlessCampaign = true,
+ formattedStartDate = "Dec 13",
+ formattedEndDate = "Dec 20, 2023"
),
onBackPressed = {},
onEditDurationTapped = {},
@@ -448,7 +644,8 @@ private fun CampaignBudgetScreenPreview() {
onStartDateChanged = {},
onUpdateTapped = {},
onBudgetChangeFinished = {},
- onApplyDurationTapped = {}
+ onApplyDurationTapped = { _, _, _ -> },
+ onDurationSliderUpdated = { _, _ -> },
)
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/budget/BlazeCampaignBudgetViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/budget/BlazeCampaignBudgetViewModel.kt
index 8d993523072..bc9829cb836 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/budget/BlazeCampaignBudgetViewModel.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/budget/BlazeCampaignBudgetViewModel.kt
@@ -8,13 +8,12 @@ import com.woocommerce.android.analytics.AnalyticsEvent.BLAZE_CREATION_EDIT_BUDG
import com.woocommerce.android.analytics.AnalyticsEvent.BLAZE_CREATION_EDIT_BUDGET_SET_DURATION_APPLIED
import com.woocommerce.android.analytics.AnalyticsTracker
import com.woocommerce.android.analytics.AnalyticsTrackerWrapper
+import com.woocommerce.android.extensions.formatToLocalizedMedium
import com.woocommerce.android.extensions.formatToMMMdd
-import com.woocommerce.android.extensions.formatToMMMddYYYY
import com.woocommerce.android.ui.blaze.BlazeRepository
import com.woocommerce.android.ui.blaze.BlazeRepository.Budget
-import com.woocommerce.android.ui.blaze.BlazeRepository.Companion.CAMPAIGN_MAXIMUM_DAILY_SPEND
import com.woocommerce.android.ui.blaze.BlazeRepository.Companion.CAMPAIGN_MAX_DURATION
-import com.woocommerce.android.ui.blaze.BlazeRepository.Companion.CAMPAIGN_MINIMUM_DAILY_SPEND
+import com.woocommerce.android.ui.blaze.BlazeRepository.Companion.WEEKLY_DURATION
import com.woocommerce.android.util.CurrencyFormatter
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ExitWithResult
@@ -25,8 +24,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
+import java.text.NumberFormat
import java.util.Date
+import java.util.Locale
import javax.inject.Inject
+import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.days
@HiltViewModel
@@ -34,7 +36,7 @@ class BlazeCampaignBudgetViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val currencyFormatter: CurrencyFormatter,
private val repository: BlazeRepository,
- private val analyticsTrackerWrapper: AnalyticsTrackerWrapper
+ private val analyticsTrackerWrapper: AnalyticsTrackerWrapper,
) : ScopedViewModel(savedStateHandle) {
companion object {
const val MAX_DATE_LIMIT_IN_DAYS = 60
@@ -47,23 +49,27 @@ class BlazeCampaignBudgetViewModel @Inject constructor(
BudgetUiState(
currencyCode = navArgs.budget.currencyCode,
totalBudget = navArgs.budget.totalBudget,
- budgetRangeMin = navArgs.budget.durationInDays * CAMPAIGN_MINIMUM_DAILY_SPEND,
- budgetRangeMax = navArgs.budget.durationInDays * CAMPAIGN_MAXIMUM_DAILY_SPEND,
- dailySpending = formatDailySpend(
- dailySpend = navArgs.budget.totalBudget / navArgs.budget.durationInDays
+ dailySpend = navArgs.budget.totalBudget / navArgs.budget.durationInDays,
+ formattedTotalBudget = formatBudget(rawValue = navArgs.budget.totalBudget),
+ formattedDailySpending = formatBudget(
+ rawValue = navArgs.budget.totalBudget / navArgs.budget.durationInDays
),
forecast = getLoadingForecastUi(),
durationInDays = navArgs.budget.durationInDays,
durationRangeMin = 1f,
durationRangeMax = CAMPAIGN_MAX_DURATION.toFloat(),
confirmedCampaignStartDateMillis = navArgs.budget.startDate.time,
- bottomSheetCampaignStartDateMillis = navArgs.budget.startDate.time,
- campaignDurationDates = getCampaignDurationDisplayDate(
- navArgs.budget.startDate.time,
- navArgs.budget.durationInDays
- ),
showImpressionsBottomSheet = false,
- showCampaignDurationBottomSheet = false
+ showCampaignDurationBottomSheet = false,
+ isEndlessCampaign = navArgs.budget.isEndlessCampaign,
+ formattedStartDate = getFormattedStartDate(
+ startDateMillis = navArgs.budget.startDate.time,
+ isEndlessCampaign = navArgs.budget.isEndlessCampaign
+ ),
+ formattedEndDate = getFormattedEndDate(
+ startDateMillis = navArgs.budget.startDate.time,
+ duration = navArgs.budget.durationInDays
+ )
)
)
@@ -92,6 +98,7 @@ class BlazeCampaignBudgetViewModel @Inject constructor(
durationInDays = budgetUiState.value.durationInDays,
startDate = Date(budgetUiState.value.confirmedCampaignStartDateMillis),
currencyCode = budgetUiState.value.currencyCode,
+ isEndlessCampaign = budgetUiState.value.isEndlessCampaign
)
)
)
@@ -100,6 +107,10 @@ class BlazeCampaignBudgetViewModel @Inject constructor(
properties = mapOf(
AnalyticsTracker.KEY_BLAZE_DURATION to budgetUiState.value.durationInDays,
AnalyticsTracker.KEY_BLAZE_TOTAL_BUDGET to budgetUiState.value.totalBudget,
+ AnalyticsTracker.KEY_BLAZE_CAMPAIGN_TYPE to when {
+ budgetUiState.value.isEndlessCampaign -> AnalyticsTracker.VALUE_EVERGREEN_CAMPAIGN
+ else -> AnalyticsTracker.VALUE_START_END_CAMPAIGN
+ },
)
)
}
@@ -122,29 +133,41 @@ class BlazeCampaignBudgetViewModel @Inject constructor(
}
}
- fun onBudgetUpdated(sliderValue: Float) {
+ fun onDailyBudgetUpdated(sliderValue: Float) {
+ val totalBudget = sliderValue.roundToInt().toFloat() * budgetUiState.value.durationInDays
budgetUiState.update {
it.copy(
- totalBudget = sliderValue,
- dailySpending = formatDailySpend(sliderValue / it.durationInDays)
+ dailySpend = sliderValue,
+ formattedDailySpending = formatBudget(sliderValue),
+ totalBudget = totalBudget,
+ formattedTotalBudget = formatBudget(totalBudget)
)
}
}
- fun onApplyDurationTapped(newDuration: Int) {
- val currentDailySpend = calculateDailySpending(newDuration)
+ fun onApplyDurationTapped(
+ newDuration: Int,
+ isEndlessCampaign: Boolean,
+ startDateMillis: Long
+ ) {
+ val duration = if (isEndlessCampaign) WEEKLY_DURATION else newDuration
+ val totalBudget = duration * budgetUiState.value.dailySpend
budgetUiState.update {
it.copy(
- durationInDays = newDuration,
- budgetRangeMin = newDuration * CAMPAIGN_MINIMUM_DAILY_SPEND,
- budgetRangeMax = newDuration * CAMPAIGN_MAXIMUM_DAILY_SPEND,
- dailySpending = formatDailySpend(currentDailySpend),
- totalBudget = newDuration * currentDailySpend,
- confirmedCampaignStartDateMillis = it.bottomSheetCampaignStartDateMillis,
- campaignDurationDates = getCampaignDurationDisplayDate(
- it.bottomSheetCampaignStartDateMillis,
- newDuration
- )
+ durationInDays = duration,
+ formattedDailySpending = formatBudget(budgetUiState.value.dailySpend),
+ totalBudget = totalBudget,
+ formattedTotalBudget = formatBudget(rawValue = totalBudget),
+ confirmedCampaignStartDateMillis = startDateMillis,
+ isEndlessCampaign = isEndlessCampaign,
+ formattedStartDate = getFormattedStartDate(
+ startDateMillis = startDateMillis,
+ isEndlessCampaign = isEndlessCampaign
+ ),
+ formattedEndDate = getFormattedEndDate(
+ startDateMillis = startDateMillis,
+ duration = duration
+ ),
)
}
fetchAdForecast()
@@ -152,6 +175,10 @@ class BlazeCampaignBudgetViewModel @Inject constructor(
stat = BLAZE_CREATION_EDIT_BUDGET_SET_DURATION_APPLIED,
properties = mapOf(
AnalyticsTracker.KEY_BLAZE_DURATION to budgetUiState.value.durationInDays,
+ AnalyticsTracker.KEY_BLAZE_CAMPAIGN_TYPE to when {
+ isEndlessCampaign -> AnalyticsTracker.VALUE_EVERGREEN_CAMPAIGN
+ else -> AnalyticsTracker.VALUE_START_END_CAMPAIGN
+ }
)
)
}
@@ -159,10 +186,9 @@ class BlazeCampaignBudgetViewModel @Inject constructor(
fun onStartDateChanged(newStartDateMillis: Long) {
budgetUiState.update {
it.copy(
- bottomSheetCampaignStartDateMillis = newStartDateMillis,
- campaignDurationDates = getCampaignDurationDisplayDate(
- newStartDateMillis,
- it.durationInDays
+ formattedEndDate = getFormattedEndDate(
+ startDateMillis = newStartDateMillis,
+ duration = it.durationInDays
)
)
}
@@ -170,16 +196,6 @@ class BlazeCampaignBudgetViewModel @Inject constructor(
fun onBudgetChangeFinished() {
fetchAdForecast()
- val roundedBudgetToDurationMultiple =
- calculateDailySpending(budgetUiState.value.durationInDays) * budgetUiState.value.durationInDays
- budgetUiState.update {
- it.copy(totalBudget = roundedBudgetToDurationMultiple)
- }
- }
-
- private fun calculateDailySpending(duration: Int): Float {
- val dailySpend = budgetUiState.value.totalBudget / duration
- return dailySpend.coerceIn(CAMPAIGN_MINIMUM_DAILY_SPEND, CAMPAIGN_MAXIMUM_DAILY_SPEND)
}
private fun fetchAdForecast() {
@@ -191,11 +207,12 @@ class BlazeCampaignBudgetViewModel @Inject constructor(
totalBudget = budgetUiState.value.totalBudget,
targetingParameters = navArgs.targetingParameters
).onSuccess { fetchAdForecastResult ->
+ val formatter = NumberFormat.getInstance(Locale.getDefault())
campaignForecastState = campaignForecastState.copy(
isLoading = false,
isError = false,
- impressionsMin = fetchAdForecastResult.minImpressions,
- impressionsMax = fetchAdForecastResult.maxImpressions
+ formattedImpressionsMin = formatter.format(fetchAdForecastResult.minImpressions),
+ formattedImpressionsMax = formatter.format(fetchAdForecastResult.maxImpressions)
)
}.onFailure {
campaignForecastState = campaignForecastState.copy(
@@ -206,28 +223,43 @@ class BlazeCampaignBudgetViewModel @Inject constructor(
}
}
- private fun getCampaignDurationDisplayDate(startDateMillis: Long, duration: Int): String {
- val endDate = Date(startDateMillis + duration.days.inWholeMilliseconds)
- return "${Date(startDateMillis).formatToMMMdd()} - ${endDate.formatToMMMddYYYY()}"
- }
+ private fun getFormattedStartDate(startDateMillis: Long, isEndlessCampaign: Boolean) =
+ when {
+ isEndlessCampaign -> Date(startDateMillis).formatToLocalizedMedium()
+ else -> Date(startDateMillis).formatToMMMdd()
+ }
+
+ private fun getFormattedEndDate(startDateMillis: Long, duration: Int) =
+ Date(startDateMillis + duration.days.inWholeMilliseconds).formatToLocalizedMedium()
- private fun formatDailySpend(dailySpend: Float) =
- currencyFormatter.formatCurrencyRounded(dailySpend.toDouble(), navArgs.budget.currencyCode)
+ private fun formatBudget(rawValue: Float) =
+ currencyFormatter.formatCurrencyRounded(rawValue.toDouble(), navArgs.budget.currencyCode)
private fun getLoadingForecastUi() = ForecastUi(
isLoading = true,
- impressionsMin = 0,
- impressionsMax = 0,
+ formattedImpressionsMin = "0",
+ formattedImpressionsMax = "0",
isError = false
)
+ fun onDurationSliderUpdated(durationInDays: Int, selectedStartDateMillis: Long) {
+ budgetUiState.update {
+ it.copy(
+ formattedEndDate = getFormattedEndDate(
+ startDateMillis = selectedStartDateMillis,
+ duration = durationInDays
+ ),
+ )
+ }
+ }
+
@Parcelize
data class BudgetUiState(
val currencyCode: String,
val totalBudget: Float,
- val budgetRangeMin: Float,
- val budgetRangeMax: Float,
- val dailySpending: String,
+ val dailySpend: Float,
+ val formattedTotalBudget: String,
+ val formattedDailySpending: String,
val forecast: ForecastUi,
val durationInDays: Int,
val durationRangeMin: Float,
@@ -235,15 +267,16 @@ class BlazeCampaignBudgetViewModel @Inject constructor(
val showImpressionsBottomSheet: Boolean,
val showCampaignDurationBottomSheet: Boolean,
val confirmedCampaignStartDateMillis: Long,
- val bottomSheetCampaignStartDateMillis: Long,
- val campaignDurationDates: String,
+ val isEndlessCampaign: Boolean,
+ val formattedStartDate: String,
+ val formattedEndDate: String
) : Parcelable
@Parcelize
data class ForecastUi(
val isLoading: Boolean,
- val impressionsMin: Long,
- val impressionsMax: Long,
+ val formattedImpressionsMin: String,
+ val formattedImpressionsMax: String,
val isError: Boolean
) : Parcelable
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/payment/BlazeCampaignPaymentMethodsListScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/payment/BlazeCampaignPaymentMethodsListScreen.kt
index faf6ed1887c..b3c1e7f3e0a 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/payment/BlazeCampaignPaymentMethodsListScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/payment/BlazeCampaignPaymentMethodsListScreen.kt
@@ -39,7 +39,7 @@ import com.woocommerce.android.ui.blaze.BlazeRepository.PaymentMethod
import com.woocommerce.android.ui.compose.component.Toolbar
import com.woocommerce.android.ui.compose.component.WCColoredButton
import com.woocommerce.android.ui.compose.component.WCTextButton
-import com.woocommerce.android.ui.compose.component.WCWebView
+import com.woocommerce.android.ui.compose.component.web.WCWebView
import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground
@Composable
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/payment/BlazeCampaignPaymentSummaryScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/payment/BlazeCampaignPaymentSummaryScreen.kt
index 283ffb5a5b6..caadcc342db 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/payment/BlazeCampaignPaymentSummaryScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/payment/BlazeCampaignPaymentSummaryScreen.kt
@@ -123,7 +123,7 @@ private fun PaymentSummaryContent(
)
PaymentTotals(
- budget = state.budgetFormatted,
+ displayBudget = state.displayBudget,
modifier = Modifier.fillMaxWidth()
)
@@ -263,7 +263,7 @@ private fun CampaignCreationErrorUi(
@Composable
private fun PaymentTotals(
- budget: String,
+ displayBudget: String,
modifier: Modifier
) {
Column(
@@ -285,7 +285,7 @@ private fun PaymentTotals(
)
Text(
- text = budget,
+ text = displayBudget,
style = MaterialTheme.typography.body2
)
}
@@ -300,7 +300,7 @@ private fun PaymentTotals(
)
Text(
- text = budget,
+ text = displayBudget,
style = MaterialTheme.typography.subtitle2
)
}
@@ -403,7 +403,7 @@ private fun BlazeCampaignPaymentSummaryScreenPreview() {
WooThemeWithBackground {
BlazeCampaignPaymentSummaryScreen(
state = BlazeCampaignPaymentSummaryViewModel.ViewState(
- budgetFormatted = "100 USD",
+ displayBudget = "100 USD",
paymentMethodsState = BlazeCampaignPaymentSummaryViewModel.PaymentMethodsState.Success(
BlazeRepository.PaymentMethodsData(
listOf(
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/payment/BlazeCampaignPaymentSummaryViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/payment/BlazeCampaignPaymentSummaryViewModel.kt
index df84b5264e1..e151408d1d1 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/payment/BlazeCampaignPaymentSummaryViewModel.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/payment/BlazeCampaignPaymentSummaryViewModel.kt
@@ -12,9 +12,11 @@ import com.woocommerce.android.model.DashboardWidget
import com.woocommerce.android.support.help.HelpOrigin
import com.woocommerce.android.ui.blaze.BlazeRepository
import com.woocommerce.android.ui.blaze.BlazeRepository.PaymentMethodsData
+import com.woocommerce.android.ui.blaze.notification.AbandonedCampaignReminder
import com.woocommerce.android.ui.dashboard.data.DashboardRepository
import com.woocommerce.android.util.CurrencyFormatter
import com.woocommerce.android.viewmodel.MultiLiveEvent
+import com.woocommerce.android.viewmodel.ResourceProvider
import com.woocommerce.android.viewmodel.ScopedViewModel
import com.woocommerce.android.viewmodel.getNullableStateFlow
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -26,16 +28,15 @@ import javax.inject.Inject
@HiltViewModel
class BlazeCampaignPaymentSummaryViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
- private val blazeRepository: BlazeRepository,
currencyFormatter: CurrencyFormatter,
+ private val blazeRepository: BlazeRepository,
+ private val abandonedCampaignReminder: AbandonedCampaignReminder,
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper,
- private val dashboardRepository: DashboardRepository
+ private val dashboardRepository: DashboardRepository,
+ private val resourceProvider: ResourceProvider
) : ScopedViewModel(savedStateHandle) {
private val navArgs = BlazeCampaignPaymentSummaryFragmentArgs.fromSavedStateHandle(savedStateHandle)
- private val budgetFormatted = currencyFormatter.formatCurrency(
- amount = navArgs.campaignDetails.budget.totalBudget.toBigDecimal(),
- currencyCode = navArgs.campaignDetails.budget.currencyCode
- )
+ private val budgetFormatted = getBudgetDisplayValue(currencyFormatter)
private val selectedPaymentMethodId = savedStateHandle.getNullableStateFlow(
scope = viewModelScope,
@@ -52,7 +53,7 @@ class BlazeCampaignPaymentSummaryViewModel @Inject constructor(
campaignCreationState
) { selectedPaymentMethodId, paymentMethodState, campaignCreationState ->
ViewState(
- budgetFormatted = budgetFormatted,
+ displayBudget = budgetFormatted,
paymentMethodsState = paymentMethodState,
selectedPaymentMethodId = selectedPaymentMethodId,
campaignCreationState = campaignCreationState
@@ -124,7 +125,17 @@ class BlazeCampaignPaymentSummaryViewModel @Inject constructor(
).fold(
onSuccess = {
campaignCreationState.value = null
- analyticsTrackerWrapper.track(stat = AnalyticsEvent.BLAZE_CAMPAIGN_CREATION_SUCCESS)
+ abandonedCampaignReminder.setBlazeCampaignCreated()
+ analyticsTrackerWrapper.track(
+ stat = AnalyticsEvent.BLAZE_CAMPAIGN_CREATION_SUCCESS,
+ properties = mapOf(
+ AnalyticsTracker.KEY_BLAZE_CAMPAIGN_TYPE to when {
+ navArgs.campaignDetails.budget.isEndlessCampaign ->
+ AnalyticsTracker.VALUE_EVERGREEN_CAMPAIGN
+ else -> AnalyticsTracker.VALUE_START_END_CAMPAIGN
+ }
+ )
+ )
dashboardRepository.updateWidgetVisibility(type = DashboardWidget.Type.ONBOARDING, isVisible = true)
triggerEvent(NavigateToStartingScreenWithSuccessBottomSheet)
},
@@ -150,8 +161,23 @@ class BlazeCampaignPaymentSummaryViewModel @Inject constructor(
}
}
+ private fun getBudgetDisplayValue(currencyFormatter: CurrencyFormatter): String {
+ val formattedBudget = currencyFormatter.formatCurrency(
+ amount = navArgs.campaignDetails.budget.totalBudget.toBigDecimal(),
+ currencyCode = navArgs.campaignDetails.budget.currencyCode
+ )
+ return when {
+ navArgs.campaignDetails.budget.isEndlessCampaign -> resourceProvider.getString(
+ R.string.blaze_campaign_budget_weekly_spending,
+ formattedBudget
+ )
+
+ else -> formattedBudget
+ }
+ }
+
data class ViewState(
- val budgetFormatted: String,
+ val displayBudget: String,
val paymentMethodsState: PaymentMethodsState,
private val selectedPaymentMethodId: String?,
val campaignCreationState: CampaignCreationState? = null
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewScreen.kt
index f57accd88c3..9865dff0cbd 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewScreen.kt
@@ -380,7 +380,8 @@ private fun CampaignPropertyItem(
Column(
Modifier
.padding(end = 16.dp)
- .weight(1f)
+ .weight(1f),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = item.displayTitle,
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewViewModel.kt
index d0060ca235a..25c770092c3 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewViewModel.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewViewModel.kt
@@ -9,6 +9,7 @@ import com.woocommerce.android.analytics.AnalyticsEvent.BLAZE_CREATION_EDIT_AD_T
import com.woocommerce.android.analytics.AnalyticsEvent.BLAZE_CREATION_FORM_DISPLAYED
import com.woocommerce.android.analytics.AnalyticsTracker
import com.woocommerce.android.analytics.AnalyticsTrackerWrapper
+import com.woocommerce.android.extensions.formatToLocalizedMedium
import com.woocommerce.android.extensions.formatToMMMdd
import com.woocommerce.android.support.help.HelpOrigin
import com.woocommerce.android.ui.blaze.BlazeRepository
@@ -165,7 +166,13 @@ class BlazeCampaignCreationPreviewViewModel @Inject constructor(
analyticsTrackerWrapper.track(
stat = BLAZE_CREATION_CONFIRM_DETAILS_TAPPED,
properties = mapOf(
- AnalyticsTracker.KEY_BLAZE_IS_AI_CONTENT to isAdContentGeneratedByAi(campaignDetails.value)
+ AnalyticsTracker.KEY_BLAZE_IS_AI_CONTENT to isAdContentGeneratedByAi(campaignDetails.value),
+ AnalyticsTracker.KEY_BLAZE_CAMPAIGN_TYPE to when {
+ campaignDetails.value?.budget?.isEndlessCampaign == true ->
+ AnalyticsTracker.VALUE_EVERGREEN_CAMPAIGN
+
+ else -> AnalyticsTracker.VALUE_START_END_CAMPAIGN
+ }
)
)
campaignDetails.value?.let {
@@ -315,12 +322,20 @@ class BlazeCampaignCreationPreviewViewModel @Inject constructor(
totalBudget.toBigDecimal(),
currencyCode
)
- val duration = resourceProvider.getString(
- R.string.blaze_campaign_preview_days_duration,
- durationInDays,
- startDate.formatToMMMdd()
- )
- return "$totalBudgetWithCurrency, $duration"
+ return when {
+ isEndlessCampaign -> resourceProvider.getString(
+ R.string.blaze_campaign_preview_days_duration_endless,
+ totalBudgetWithCurrency,
+ startDate.formatToLocalizedMedium()
+ )
+
+ else ->
+ "$totalBudgetWithCurrency, " + resourceProvider.getString(
+ R.string.blaze_campaign_preview_days_duration,
+ durationInDays,
+ startDate.formatToMMMdd()
+ )
+ }
}
data class CampaignPreviewUiState(
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/notification/AbandonedCampaignReminder.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/notification/AbandonedCampaignReminder.kt
new file mode 100644
index 00000000000..4fcf16521ca
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/notification/AbandonedCampaignReminder.kt
@@ -0,0 +1,30 @@
+package com.woocommerce.android.ui.blaze.notification
+
+import com.woocommerce.android.AppPrefsWrapper
+import com.woocommerce.android.notifications.local.LocalNotification.BlazeAbandonedCampaignReminderNotification
+import com.woocommerce.android.notifications.local.LocalNotificationScheduler
+import com.woocommerce.android.tools.SelectedSite
+import javax.inject.Inject
+
+/**
+ * A utility class that schedules a reminder after a campaign is abandoned.
+ */
+class AbandonedCampaignReminder @Inject constructor(
+ private val selectedSite: SelectedSite,
+ private val appPrefsWrapper: AppPrefsWrapper,
+ private val localNotificationScheduler: LocalNotificationScheduler,
+) {
+ private val notification
+ get() = BlazeAbandonedCampaignReminderNotification(selectedSite.get().siteId)
+
+ fun scheduleReminderIfNeeded() {
+ if (!appPrefsWrapper.getBlazeCampaignCreated() && !appPrefsWrapper.isBlazeAbandonedCampaignReminderShown) {
+ localNotificationScheduler.scheduleNotification(notification)
+ }
+ }
+
+ fun setBlazeCampaignCreated() {
+ appPrefsWrapper.setBlazeCampaignCreated()
+ localNotificationScheduler.cancelScheduledNotification(notification.type)
+ }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/notification/BlazeCampaignsObserver.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/notification/BlazeCampaignsObserver.kt
index 37f07d16977..40f6e84a014 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/notification/BlazeCampaignsObserver.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/notification/BlazeCampaignsObserver.kt
@@ -4,15 +4,18 @@ import com.woocommerce.android.AppPrefsWrapper
import com.woocommerce.android.extensions.daysLater
import com.woocommerce.android.notifications.local.LocalNotification.BlazeNoCampaignReminderNotification
import com.woocommerce.android.notifications.local.LocalNotificationScheduler
+import com.woocommerce.android.notifications.local.LocalNotificationType
import com.woocommerce.android.tools.SelectedSite
+import com.woocommerce.android.ui.blaze.CampaignStatusUi
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import org.wordpress.android.fluxc.model.SiteModel
-import org.wordpress.android.fluxc.persistence.blaze.BlazeCampaignsDao
+import org.wordpress.android.fluxc.model.blaze.BlazeCampaignModel
import org.wordpress.android.fluxc.store.blaze.BlazeCampaignsStore
import java.util.Calendar
+import java.util.concurrent.TimeUnit
import javax.inject.Inject
/**
@@ -28,7 +31,7 @@ class BlazeCampaignsObserver @Inject constructor(
suspend fun observeAndScheduleNotifications() {
selectedSite.observe()
.filterNotNull()
- .filter { !appPrefsWrapper.getBlazeNoCampaignReminderShown(it.siteId) }
+ .filter { !appPrefsWrapper.isBlazeNoCampaignReminderShown }
.distinctUntilChanged { old, new -> new.id == old.id }
.collectLatest { observeBlazeCampaigns(it) }
}
@@ -36,29 +39,57 @@ class BlazeCampaignsObserver @Inject constructor(
private suspend fun observeBlazeCampaigns(site: SiteModel) {
blazeCampaignsStore.observeBlazeCampaigns(site)
.filter { it.isNotEmpty() }
- .collectLatest { scheduleNotification(it) }
+ .collectLatest { processCampaigns(it) }
}
- private fun scheduleNotification(campaigns: List) {
+ private fun processCampaigns(campaigns: List) {
if (campaigns.isEmpty()) {
// There are no campaigns. Skip scheduling the notification.
return
+ } else if (hasActiveEndlessCampaigns(campaigns)) {
+ appPrefsWrapper.removeBlazeFirstTimeWithoutCampaign()
+ localNotificationScheduler.cancelScheduledNotification(LocalNotificationType.BLAZE_NO_CAMPAIGN_REMINDER)
+ } else if (campaigns.any { CampaignStatusUi.isActive(it.uiStatus) }) {
+ // There are active limited campaigns.
+ val latestEndTime = getLatestEndTimeOfActiveLimitedCampaigns(campaigns)
+ scheduleNotification(latestEndTime)
+ } else if (!appPrefsWrapper.existsBlazeFirstTimeWithoutCampaign() ||
+ appPrefsWrapper.blazeFirstTimeWithoutCampaign > Calendar.getInstance().time.time
+ ) {
+ scheduleNotification(Calendar.getInstance().time.time)
}
+ }
- val delayForNotification = calculateDelayForNotification(campaigns)
+ private fun hasActiveEndlessCampaigns(campaigns: List) = campaigns.any {
+ it.isEndlessCampaign && CampaignStatusUi.isActive(it.uiStatus)
+ }
- localNotificationScheduler.scheduleNotification(
- BlazeNoCampaignReminderNotification(selectedSite.get().siteId, delayForNotification)
- )
+ private fun getLatestEndTimeOfActiveLimitedCampaigns(campaigns: List): Long {
+ val activeLimitedCampaigns = campaigns.filter {
+ !it.isEndlessCampaign && CampaignStatusUi.isActive(it.uiStatus)
+ }
+ return activeLimitedCampaigns.maxOf { it.startTime.daysLater(it.durationInDays) }.time
}
- private fun calculateDelayForNotification(campaigns: List): Long {
- val latestEndTime = campaigns.maxOf { it.startTime.daysLater(it.durationInDays) }
- val notificationTime = latestEndTime.daysLater(DAYS_DURATION_NO_CAMPAIGN_REMINDER_NOTIFICATION)
- return notificationTime.time - Calendar.getInstance().time.time
+ private fun scheduleNotification(firstTimeWithoutCampaign: Long) {
+ if (appPrefsWrapper.blazeFirstTimeWithoutCampaign == firstTimeWithoutCampaign) {
+ // There is already a scheduled notification for firstTimeWithoutCampaign.
+ return
+ }
+ appPrefsWrapper.blazeFirstTimeWithoutCampaign = firstTimeWithoutCampaign
+
+ val notificationTime = firstTimeWithoutCampaign +
+ TimeUnit.DAYS.toMillis(DAYS_DURATION_NO_CAMPAIGN_REMINDER_NOTIFICATION)
+
+ val notification = BlazeNoCampaignReminderNotification(
+ siteId = selectedSite.get().siteId,
+ delay = notificationTime - Calendar.getInstance().time.time
+ )
+
+ localNotificationScheduler.scheduleNotification(notification)
}
companion object {
- const val DAYS_DURATION_NO_CAMPAIGN_REMINDER_NOTIFICATION = 30
+ private const val DAYS_DURATION_NO_CAMPAIGN_REMINDER_NOTIFICATION = 30L
}
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/common/wpcomwebview/WPComWebViewScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/common/wpcomwebview/WPComWebViewScreen.kt
index a9e008e94f0..eabef5c24d8 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/common/wpcomwebview/WPComWebViewScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/common/wpcomwebview/WPComWebViewScreen.kt
@@ -14,7 +14,7 @@ import com.woocommerce.android.R
import com.woocommerce.android.ui.common.wpcomwebview.WPComWebViewViewModel.DisplayMode.MODAL
import com.woocommerce.android.ui.common.wpcomwebview.WPComWebViewViewModel.DisplayMode.REGULAR
import com.woocommerce.android.ui.compose.component.Toolbar
-import com.woocommerce.android.ui.compose.component.WCWebView
+import com.woocommerce.android.ui.compose.component.web.WCWebView
import org.wordpress.android.fluxc.network.UserAgent
@Composable
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/ExpandableTopBanner.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/ExpandableTopBanner.kt
new file mode 100644
index 00000000000..4bdd3ad03c5
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/ExpandableTopBanner.kt
@@ -0,0 +1,127 @@
+package com.woocommerce.android.ui.compose.component
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.Card
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Icon
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Campaign
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.woocommerce.android.R
+import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews
+import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground
+
+@Composable
+fun ExpandableTopBanner(
+ title: String,
+ message: String,
+ buttons: @Composable RowScope.() -> Unit,
+ modifier: Modifier = Modifier,
+ expandedByDefault: Boolean = false,
+) {
+ Card(
+ shape = RectangleShape,
+ modifier = modifier
+ ) {
+ var isExpanded by rememberSaveable { mutableStateOf(expandedByDefault) }
+
+ Column {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clickable { isExpanded = !isExpanded }
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Campaign,
+ contentDescription = null,
+ tint = MaterialTheme.colors.primary
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = title,
+ style = MaterialTheme.typography.subtitle1,
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Icon(
+ imageVector = if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
+ contentDescription = null,
+ tint = MaterialTheme.colors.primary
+ )
+ }
+
+ AnimatedVisibility(visible = isExpanded) {
+ Text(
+ text = message,
+ style = MaterialTheme.typography.body2,
+ color = LocalContentColor.current.copy(alpha = ContentAlpha.medium),
+ modifier = Modifier.padding(start = 48.dp, end = 16.dp)
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .align(Alignment.End)
+ .padding(horizontal = 16.dp)
+ ) {
+ buttons()
+ }
+ }
+ }
+}
+
+@Composable
+fun ExpandableTopBanner(
+ title: String,
+ message: String,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+ expandedByDefault: Boolean = false,
+) {
+ ExpandableTopBanner(
+ title = title,
+ message = message,
+ buttons = {
+ WCTextButton(
+ onClick = onDismiss,
+ ) {
+ Text(stringResource(id = R.string.dismiss))
+ }
+ },
+ modifier = modifier,
+ expandedByDefault = expandedByDefault,
+ )
+}
+
+@Composable
+@LightDarkThemePreviews
+private fun ExpandableTopBannerPreview() {
+ WooThemeWithBackground {
+ ExpandableTopBanner(
+ title = "Title",
+ message = "Message",
+ onDismiss = {},
+ )
+ }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/ModalStatusBarBottomSheetLayout.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/ModalStatusBarBottomSheetLayout.kt
index d00e3354df0..08877698aaa 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/ModalStatusBarBottomSheetLayout.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/ModalStatusBarBottomSheetLayout.kt
@@ -1,9 +1,6 @@
package com.woocommerce.android.ui.compose.component
import android.R.attr
-import android.app.Activity
-import android.content.Context
-import android.content.ContextWrapper
import android.util.TypedValue
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
@@ -26,6 +23,7 @@ import androidx.compose.material.contentColorFor
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -39,6 +37,7 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.Dp
import androidx.core.view.WindowCompat
import com.woocommerce.android.R
+import com.woocommerce.android.extensions.findActivity
/*
* This is a custom implementation of the ModalBottomSheetLayout that fixes the scrim color of the status bar
@@ -83,7 +82,7 @@ fun ModalStatusBarBottomSheetLayout(
var statusBarColor by remember { mutableStateOf(Color.Transparent) }
val backgroundColor = remember {
val typedValue = TypedValue()
- if (context.findActivity().theme.resolveAttribute(attr.windowBackground, typedValue, true)) {
+ if (context.findActivity()?.theme?.resolveAttribute(attr.windowBackground, typedValue, true) == true) {
Color(typedValue.data)
} else {
sheetBackgroundColor
@@ -106,12 +105,17 @@ fun ModalStatusBarBottomSheetLayout(
}
}
- val window = remember { context.findActivity().window }
+ val window = remember { context.findActivity()?.window }
+ if (window == null) return@ModalBottomSheetLayout
+
val originalNavigationBarColor = remember { window.navigationBarColor }
- if (sheetState.currentValue != Hidden) {
- window.navigationBarColor = sheetBackgroundColor.toArgb()
- } else {
- window.navigationBarColor = originalNavigationBarColor
+
+ LaunchedEffect(sheetState.currentValue) {
+ if (sheetState.currentValue != Hidden) {
+ window.navigationBarColor = sheetBackgroundColor.toArgb()
+ } else {
+ window.navigationBarColor = originalNavigationBarColor
+ }
}
DisposableEffect(Unit) {
@@ -135,12 +139,3 @@ private fun scrimColor() = if (isSystemInDarkTheme()) {
} else {
ModalBottomSheetDefaults.scrimColor
}
-
-fun Context.findActivity(): Activity {
- var context = this
- while (context is ContextWrapper) {
- if (context is Activity) return context
- context = context.baseContext
- }
- error("Permissions should be called in the context of an Activity")
-}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/OverflowMenu.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/OverflowMenu.kt
index 428dbc1a59a..e13f8ea6199 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/OverflowMenu.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/OverflowMenu.kt
@@ -7,6 +7,8 @@ import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons.Outlined
import androidx.compose.material.icons.outlined.MoreVert
@@ -30,7 +32,8 @@ fun WCOverflowMenu(
onSelected: (T) -> Unit,
modifier: Modifier = Modifier,
mapper: @Composable (T) -> String = { it.toString() },
- tint: Color = Color.Black
+ itemColor: @Composable (T) -> Color = { LocalContentColor.current },
+ tint: Color = MaterialTheme.colors.primary
) {
var showMenu by remember { mutableStateOf(false) }
Box(modifier = modifier) {
@@ -57,7 +60,10 @@ fun WCOverflowMenu(
onSelected(item)
}
) {
- Text(mapper(item))
+ Text(
+ text = mapper(item),
+ color = itemColor(item)
+ )
}
if (index < items.size - 1) {
Spacer(modifier = Modifier.height(dimensionResource(id = dimen.minor_100)))
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/Switch.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/Switch.kt
index c572832f607..94004dd1b1b 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/Switch.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/Switch.kt
@@ -1,6 +1,7 @@
package com.woocommerce.android.ui.compose.component
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material.MaterialTheme
@@ -12,11 +13,25 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.colorResource
+import com.woocommerce.android.R
@Composable
private fun defaultSwitchColors(): SwitchColors =
SwitchDefaults.colors(checkedThumbColor = MaterialTheme.colors.primary)
+@Composable
+fun BottomSheetSwitchColors(): SwitchColors =
+ SwitchDefaults.colors(
+ checkedThumbColor = colorResource(id = R.color.color_primary),
+ checkedTrackColor = colorResource(id = R.color.color_primary),
+ uncheckedThumbColor =
+ when {
+ isSystemInDarkTheme() -> colorResource(id = R.color.color_on_surface_medium)
+ else -> MaterialTheme.colors.onSurface
+ }
+ )
+
@Composable
fun WCSwitch(
checked: Boolean,
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/WebChromeClientWithImageChooser.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/WebChromeClientWithImageChooser.kt
deleted file mode 100644
index 530184c77b7..00000000000
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/WebChromeClientWithImageChooser.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.woocommerce.android.ui.compose.component
-
-import android.content.ActivityNotFoundException
-import android.net.Uri
-import android.webkit.ValueCallback
-import android.webkit.WebChromeClient
-import android.webkit.WebView
-import androidx.activity.result.ActivityResultRegistry
-import androidx.activity.result.contract.ActivityResultContracts.GetContent
-import com.woocommerce.android.util.WooLog
-import com.woocommerce.android.util.WooLog.T
-
-class WebChromeClientWithImageChooser(
- registry: ActivityResultRegistry,
- private val onProgressChanged: (Int) -> Unit
-) : WebChromeClient() {
- companion object {
- private const val FILE_CHOOSER_RESULT_KEY = "file_chooser_result_key"
- }
-
- private lateinit var fileChooserValueCallback: ValueCallback>
- private val getImageContent = registry.register(FILE_CHOOSER_RESULT_KEY, GetContent()) { uri ->
- uri?.let {
- fileChooserValueCallback.onReceiveValue(arrayOf(uri))
- } ?: fileChooserValueCallback.onReceiveValue(null)
- }
-
- override fun onShowFileChooser(
- webView: WebView?,
- filePathCallback: ValueCallback>,
- fileChooserParams: FileChooserParams
- ): Boolean {
- try {
- fileChooserValueCallback = filePathCallback
- getImageContent.launch("image/*")
- } catch (e: ActivityNotFoundException) {
- WooLog.d(
- T.UTILS,
- "WebChromeClientWithImageChooser. No activity found to handle image selection: ${e.message}"
- )
- }
- return true
- }
-
- override fun onProgressChanged(view: WebView?, newProgress: Int) {
- onProgressChanged(newProgress)
- }
-}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/aztec/AztecEditor.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/aztec/AztecEditor.kt
new file mode 100644
index 00000000000..788b0d7517c
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/aztec/AztecEditor.kt
@@ -0,0 +1,451 @@
+package com.woocommerce.android.ui.compose.component.aztec
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View.OnFocusChangeListener
+import android.view.ViewGroup
+import android.widget.EditText
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.isImeVisible
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.foundation.relocation.bringIntoViewRequester
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalTextInputService
+import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.input.TextInputService
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.widget.doAfterTextChanged
+import com.google.android.material.textfield.TextInputLayout
+import com.woocommerce.android.databinding.ViewAztecBinding
+import com.woocommerce.android.databinding.ViewAztecOutlinedBinding
+import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import org.wordpress.aztec.Aztec
+import org.wordpress.aztec.AztecText
+import org.wordpress.aztec.ITextFormat
+import org.wordpress.aztec.glideloader.GlideImageLoader
+import org.wordpress.aztec.source.SourceViewEditText
+import org.wordpress.aztec.toolbar.IAztecToolbar
+import org.wordpress.aztec.toolbar.IAztecToolbarClickListener
+
+/**
+ * An Aztec editor that can be used in Compose, with an outlined style.
+ *
+ * @param content The content of the editor
+ * @param onContentChanged A callback that will be called when the content of the editor changes
+ * @param modifier The modifier to apply to the editor
+ * @param label The label to display above the editor
+ * @param minLines The minimum number of lines the editor should have
+ * @param maxLines The maximum number of lines the editor should have
+ * @param calypsoMode Whether the editor should be in calypso mode, for more information on calypso mode see https://github.com/wordpress-mobile/AztecEditor-Android/pull/309
+ */
+@Composable
+fun OutlinedAztecEditor(
+ content: String,
+ onContentChanged: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ label: String? = null,
+ minLines: Int = 1,
+ maxLines: Int = Int.MAX_VALUE,
+ calypsoMode: Boolean = false
+) {
+ val state = rememberAztecEditorState(initialContent = content)
+ val contentState by rememberUpdatedState(content)
+
+ LaunchedEffect(Unit) {
+ snapshotFlow { contentState }
+ .onEach { state.updateContent(it) }
+ .launchIn(this)
+
+ snapshotFlow { state.content }
+ .onEach { onContentChanged(it) }
+ .launchIn(this)
+ }
+
+ OutlinedAztecEditor(
+ state = state,
+ modifier = modifier,
+ label = label,
+ minLines = minLines,
+ maxLines = maxLines,
+ calypsoMode = calypsoMode
+ )
+}
+
+/**
+ * An Aztec editor that can be used in Compose, with an outlined style.
+ *
+ * @param state The state of the editor, see [rememberAztecEditorState]
+ * @param modifier The modifier to apply to the editor
+ * @param label The label to display above the editor
+ * @param minLines The minimum number of lines the editor should have
+ * @param maxLines The maximum number of lines the editor should have
+ * @param calypsoMode Whether the editor should be in calypso mode, for more information on calypso mode see https://github.com/wordpress-mobile/AztecEditor-Android/pull/309
+ */
+@Composable
+fun OutlinedAztecEditor(
+ state: AztecEditorState,
+ modifier: Modifier = Modifier,
+ label: String? = null,
+ minLines: Int = 1,
+ maxLines: Int = Int.MAX_VALUE,
+ calypsoMode: Boolean = false
+) {
+ InternalAztecEditor(
+ state = state,
+ aztecViewsProvider = { context ->
+ val binding = ViewAztecOutlinedBinding.inflate(LayoutInflater.from(context)).apply {
+ visualEditor.background = null
+ sourceEditor.background = null
+ }
+
+ AztecViewsHolder(
+ layout = binding.root,
+ visualEditor = binding.visualEditor,
+ sourceEditor = binding.sourceEditor,
+ toolbar = binding.toolbar
+ )
+ },
+ modifier = modifier,
+ label = label,
+ minLines = minLines,
+ maxLines = maxLines,
+ calypsoMode = calypsoMode
+ )
+}
+
+/**
+ * An Aztec editor that can be used in Compose.
+ *
+ * @param content The content of the editor
+ * @param onContentChanged A callback that will be called when the content of the editor changes
+ * @param modifier The modifier to apply to the editor
+ * @param label The label to display above the editor
+ * @param minLines The minimum number of lines the editor should have
+ * @param maxLines The maximum number of lines the editor should have
+ * @param calypsoMode Whether the editor should be in calypso mode, for more information on calypso mode see https://github.com/wordpress-mobile/AztecEditor-Android/pull/309
+ */
+@Composable
+fun AztecEditor(
+ content: String,
+ onContentChanged: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ label: String? = null,
+ minLines: Int = 1,
+ maxLines: Int = Int.MAX_VALUE,
+ calypsoMode: Boolean = false
+) {
+ val state = rememberAztecEditorState(initialContent = content)
+ val contentState by rememberUpdatedState(content)
+
+ LaunchedEffect(Unit) {
+ snapshotFlow { contentState }
+ .onEach { state.updateContent(it) }
+ .launchIn(this)
+
+ snapshotFlow { state.content }
+ .onEach { onContentChanged(it) }
+ .launchIn(this)
+ }
+
+ AztecEditor(
+ state = state,
+ modifier = modifier,
+ label = label,
+ minLines = minLines,
+ maxLines = maxLines,
+ calypsoMode = calypsoMode
+ )
+}
+
+/**
+ * An Aztec editor that can be used in Compose.
+ *
+ * @param state The state of the editor, see [rememberAztecEditorState]
+ * @param modifier The modifier to apply to the editor
+ * @param label The label to display above the editor
+ * @param minLines The minimum number of lines the editor should have
+ * @param maxLines The maximum number of lines the editor should have
+ * @param calypsoMode Whether the editor should be in calypso mode, for more information on calypso mode see https://github.com/wordpress-mobile/AztecEditor-Android/pull/309
+ */
+@Composable
+fun AztecEditor(
+ state: AztecEditorState,
+ modifier: Modifier = Modifier,
+ label: String? = null,
+ minLines: Int = 1,
+ maxLines: Int = Int.MAX_VALUE,
+ calypsoMode: Boolean = false
+) {
+ InternalAztecEditor(
+ state = state,
+ aztecViewsProvider = { context ->
+ val binding = ViewAztecBinding.inflate(LayoutInflater.from(context))
+
+ AztecViewsHolder(
+ layout = binding.root,
+ visualEditor = binding.visualEditor,
+ sourceEditor = binding.sourceEditor,
+ toolbar = binding.toolbar
+ )
+ },
+ modifier = modifier,
+ label = label,
+ minLines = minLines,
+ maxLines = maxLines,
+ calypsoMode = calypsoMode
+ )
+}
+
+@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
+@Composable
+private fun InternalAztecEditor(
+ state: AztecEditorState,
+ aztecViewsProvider: (context: Context) -> AztecViewsHolder,
+ modifier: Modifier = Modifier,
+ label: String? = null,
+ minLines: Int = 1,
+ maxLines: Int = Int.MAX_VALUE,
+ calypsoMode: Boolean = false
+) {
+ val localContext = LocalContext.current
+ val bringIntoViewRequester = remember { BringIntoViewRequester() }
+ val textInputService = LocalTextInputService.current
+
+ val viewsHolder = remember(localContext) { aztecViewsProvider(localContext) }
+ val listener = remember { createToolbarListener { state.toggleHtmlEditor() } }
+ val aztec = remember(localContext) {
+ Aztec.with(viewsHolder.visualEditor, viewsHolder.sourceEditor, viewsHolder.toolbar, listener)
+ .setImageGetter(GlideImageLoader(localContext))
+ }
+ var sourceEditorMinHeight by rememberSaveable { mutableStateOf(0) }
+
+ // Toggle the editor mode when the state changes
+ LaunchedEffect(Unit) {
+ snapshotFlow { state.isHtmlEditorEnabled }
+ .drop(1) // Skip the initial value to avoid toggling the editor when it's first created
+ .collect { aztec.toolbar.toggleEditorMode() }
+ }
+
+ // Update the content of the editor when the state changes
+ LaunchedEffect(state.content) {
+ if (state.isHtmlEditorEnabled) {
+ if (aztec.visualEditor.toHtml() != state.content) {
+ aztec.visualEditor.fromHtml(state.content)
+ }
+ } else {
+ if (aztec.sourceEditor?.getPureHtml() != state.content) {
+ aztec.sourceEditor?.displayStyledAndFormattedHtml(state.content)
+ }
+ }
+ }
+
+ val focusState = remember { MutableStateFlow(false) }
+ val isImeVisible = rememberUpdatedState(WindowInsets.isImeVisible)
+
+ LaunchedEffect(Unit) {
+ handleFocus(
+ focusState = focusState,
+ imeVisibility = isImeVisible,
+ bringIntoViewRequester = bringIntoViewRequester,
+ textInputService = textInputService
+ )
+ }
+
+ AndroidView(
+ factory = {
+ // Set initial content
+ aztec.visualEditor.fromHtml(state.content)
+ aztec.sourceEditor?.displayStyledAndFormattedHtml(state.content)
+
+ aztec.visualEditor.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+ // Because the editors could have different number of lines, we don't set the minLines
+ // of the source editor, so we set the minHeight instead to match the visual editor
+ sourceEditorMinHeight = aztec.visualEditor.height
+ }
+
+ aztec.visualEditor.doAfterTextChanged {
+ if (!state.isHtmlEditorEnabled) return@doAfterTextChanged
+ state.updateContent(aztec.visualEditor.toHtml())
+ }
+ aztec.sourceEditor?.doAfterTextChanged {
+ val sourceEditor = aztec.sourceEditor
+ if (state.isHtmlEditorEnabled || sourceEditor == null) return@doAfterTextChanged
+ state.updateContent(sourceEditor.getPureHtml())
+ }
+
+ val focusChangeListener = OnFocusChangeListener { _, focused ->
+ focusState.value = focused
+ }
+ aztec.visualEditor.onFocusChangeListener = focusChangeListener
+ aztec.sourceEditor?.onFocusChangeListener = focusChangeListener
+
+ viewsHolder.layout
+ },
+ update = {
+ if (aztec.visualEditor.isInCalypsoMode != calypsoMode) {
+ aztec.visualEditor.isInCalypsoMode = calypsoMode
+ aztec.sourceEditor?.setCalypsoMode(calypsoMode)
+ }
+
+ if (sourceEditorMinHeight != aztec.sourceEditor?.minHeight) {
+ aztec.sourceEditor?.minHeight = sourceEditorMinHeight
+ }
+ if (minLines != -1 && minLines != aztec.visualEditor.minLines) {
+ aztec.visualEditor.minLines = minLines
+ }
+ if (maxLines != Int.MAX_VALUE && maxLines != aztec.visualEditor.maxLines) {
+ aztec.visualEditor.maxLines = maxLines
+ aztec.sourceEditor?.maxLines = maxLines
+ }
+
+ if (aztec.visualEditor.label != label) {
+ aztec.visualEditor.label = label
+ aztec.sourceEditor?.label = label
+ }
+ },
+ modifier = modifier
+ .bringIntoViewRequester(bringIntoViewRequester)
+ )
+}
+
+@OptIn(ExperimentalFoundationApi::class, InternalTextApi::class)
+private suspend fun handleFocus(
+ focusState: StateFlow,
+ imeVisibility: State,
+ bringIntoViewRequester: BringIntoViewRequester,
+ textInputService: TextInputService?
+) = coroutineScope {
+ launch(Dispatchers.Main.immediate) {
+ // In Compose, text fields use input sessions to manage the input, when focus moves to a non-input field
+ // the session is closed and this hides the keyboard.
+ // This behavior doesn't work well when focus moves to a non-Compose input field, like the Aztec editor.
+ // see: https://issuetracker.google.com/issues/318530776 and https://issuetracker.google.com/issues/363544352
+ // To get around the issue, we are using the internal API to start/stop the input session.
+ // This is safe to do because even if the API changes, we can remove the logic temporarily until the bug is
+ // fixed, as this bug is not critical for the editor.
+ focusState.collect {
+ if (it) {
+ textInputService?.startInput()
+ } else {
+ textInputService?.stopInput()
+ }
+ }
+ }
+
+ launch {
+ // Use collectLatest to make sure the nested collection is cancelled when the focus state changes
+ focusState.collectLatest { hasFocus ->
+ if (!hasFocus) return@collectLatest
+ bringIntoViewRequester.bringIntoView()
+
+ snapshotFlow { imeVisibility.value }
+ .filter { it }
+ .collect { bringIntoViewRequester.bringIntoView() }
+ }
+ }
+}
+
+private fun createToolbarListener(onHtmlButtonClicked: () -> Unit) = object : IAztecToolbarClickListener {
+ override fun onToolbarCollapseButtonClicked() = Unit
+
+ override fun onToolbarExpandButtonClicked() = Unit
+
+ override fun onToolbarFormatButtonClicked(format: ITextFormat, isKeyboardShortcut: Boolean) = Unit
+
+ override fun onToolbarHeadingButtonClicked() = Unit
+
+ override fun onToolbarHtmlButtonClicked() {
+ onHtmlButtonClicked()
+ }
+
+ override fun onToolbarListButtonClicked() = Unit
+
+ override fun onToolbarMediaButtonClicked(): Boolean = false
+}
+
+/**
+ * Helper to set the label of an [EditText] depending on whether it is wrapped in a [TextInputLayout]
+ */
+private var EditText.label
+ get() = (parent?.parent as? TextInputLayout)?.hint ?: hint
+ set(value) {
+ (parent?.parent as? TextInputLayout)?.let { it.hint = value } ?: run {
+ hint = value
+ }
+ }
+
+private data class AztecViewsHolder(
+ val layout: ViewGroup,
+ val visualEditor: AztecText,
+ val sourceEditor: SourceViewEditText,
+ val toolbar: IAztecToolbar
+)
+
+@Composable
+@Preview
+private fun OutlinedAztecEditorPreview() {
+ val state = rememberAztecEditorState("something")
+
+ WooThemeWithBackground {
+ Column {
+ OutlinedAztecEditor(
+ state = state,
+ label = "Label",
+ minLines = 5,
+ modifier = Modifier.padding(16.dp)
+ )
+
+ TextButton(onClick = { state.toggleHtmlEditor() }) {
+ Text("Toggle Html Mode")
+ }
+ }
+ }
+}
+
+@Composable
+@Preview
+private fun AztecEditorPreview() {
+ val state = rememberAztecEditorState("")
+
+ WooThemeWithBackground {
+ Column {
+ AztecEditor(
+ state = state,
+ label = "Label",
+ )
+
+ TextButton(onClick = { state.toggleHtmlEditor() }) {
+ Text("Toggle Html Mode")
+ }
+ }
+ }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/aztec/AztecEditorState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/aztec/AztecEditorState.kt
new file mode 100644
index 00000000000..b2979f7ed07
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/aztec/AztecEditorState.kt
@@ -0,0 +1,51 @@
+package com.woocommerce.android.ui.compose.component.aztec
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+
+class AztecEditorState(
+ initialContent: String
+) {
+ var content by mutableStateOf(initialContent)
+ private set
+
+ var isHtmlEditorEnabled by mutableStateOf(true)
+ private set
+
+ fun updateContent(newContent: String) {
+ content = newContent
+ }
+
+ fun toggleHtmlEditor() {
+ isHtmlEditorEnabled = !isHtmlEditorEnabled
+ }
+
+ companion object {
+ val Saver = Saver(
+ save = { arrayListOf(it.content, it.isHtmlEditorEnabled) },
+ restore = {
+ val list = it as List<*>
+ AztecEditorState(list[0] as String).apply {
+ isHtmlEditorEnabled = list[1] as Boolean
+ }
+ }
+ )
+ }
+}
+
+/**
+ * Remember a [AztecEditorState] that can be used to manage the state of an Aztec editor.
+ * The state will be saved to the saved state to survive process death.
+ *
+ * @param initialContent The initial content of the editor
+ */
+@Composable
+fun rememberAztecEditorState(
+ initialContent: String
+): AztecEditorState {
+ return rememberSaveable(saver = AztecEditorState.Saver) { AztecEditorState(initialContent) }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/web/ComposeWebChromeClient.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/web/ComposeWebChromeClient.kt
new file mode 100644
index 00000000000..bfb7600ade9
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/web/ComposeWebChromeClient.kt
@@ -0,0 +1,51 @@
+package com.woocommerce.android.ui.compose.component.web
+
+import android.net.Uri
+import android.webkit.ValueCallback
+import android.webkit.WebChromeClient
+import android.webkit.WebView
+import androidx.activity.ComponentActivity
+import androidx.activity.result.contract.ActivityResultContracts
+import com.woocommerce.android.extensions.findActivity
+
+open class ComposeWebChromeClient : WebChromeClient() {
+ var onProgressChanged: (Int) -> Unit = {}
+
+ /**
+ * This method is called when the user chooses a file for file upload.
+ *
+ * Important: this implementation doesn't handle configuration changes, during which the flow
+ * will just be interrupted.
+ * Our WebView implementation doesn't survive configuration changes either, so this is fine for now.
+ *
+ * If we need to handle configuration changes, we'll need the following:
+ * - Find a way to keep the WebView instance across configuration changes.
+ * - Store the [filePathCallback] during configuration changes (possibly in a ViewModel).
+ * - Move the ActivityResultLauncher outside of this method to make sure it's called after the configuration change.
+ */
+ override fun onShowFileChooser(
+ webView: WebView,
+ filePathCallback: ValueCallback>,
+ fileChooserParams: FileChooserParams
+ ): Boolean {
+ val activity = webView.context.findActivity() as? ComponentActivity ?: return false
+
+ val contract = ActivityResultContracts.StartActivityForResult()
+ val launcher = activity.activityResultRegistry.register(
+ "WebViewChooser",
+ contract
+ ) { result ->
+ val uris = FileChooserParams.parseResult(result.resultCode, result.data)
+ filePathCallback.onReceiveValue(uris)
+ }
+
+ val intent = fileChooserParams.createIntent()
+ launcher.launch(intent)
+
+ return true
+ }
+
+ override fun onProgressChanged(view: WebView?, newProgress: Int) {
+ onProgressChanged(newProgress)
+ }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/WebView.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/web/WebView.kt
similarity index 90%
rename from WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/WebView.kt
rename to WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/web/WebView.kt
index 4abba3481ca..5a440cc8ff9 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/WebView.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/web/WebView.kt
@@ -1,9 +1,8 @@
-package com.woocommerce.android.ui.compose.component
+package com.woocommerce.android.ui.compose.component.web
import android.annotation.SuppressLint
import android.view.ViewGroup
import android.webkit.CookieManager
-import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
@@ -11,7 +10,6 @@ import android.webkit.WebStorage
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
-import androidx.activity.result.ActivityResultRegistry
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@@ -33,8 +31,8 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.viewinterop.AndroidView
import com.woocommerce.android.R.dimen
import com.woocommerce.android.ui.common.wpcomwebview.WPComWebViewAuthenticator
-import com.woocommerce.android.ui.compose.component.WebViewProgressIndicator.Circular
-import com.woocommerce.android.ui.compose.component.WebViewProgressIndicator.Linear
+import com.woocommerce.android.ui.compose.component.web.WebViewProgressIndicator.Circular
+import com.woocommerce.android.ui.compose.component.web.WebViewProgressIndicator.Linear
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onCompletion
import org.wordpress.android.fluxc.network.UserAgent
@@ -52,7 +50,7 @@ fun WCWebView(
captureBackPresses: Boolean = true,
wpComAuthenticator: WPComWebViewAuthenticator? = null,
webViewNavigator: WebViewNavigator = rememberWebViewNavigator(),
- activityRegistry: ActivityResultRegistry? = null,
+ webChromeClient: ComposeWebChromeClient = remember { ComposeWebChromeClient() },
loadWithOverviewMode: Boolean = false,
useWideViewPort: Boolean = false,
isJavaScriptEnabled: Boolean = true,
@@ -135,15 +133,8 @@ fun WCWebView(
}
}
- if (activityRegistry != null) {
- this.webChromeClient =
- WebChromeClientWithImageChooser(activityRegistry) { newProgress -> progress = newProgress }
- } else {
- this.webChromeClient = object : WebChromeClient() {
- override fun onProgressChanged(view: WebView?, newProgress: Int) {
- progress = newProgress
- }
- }
+ this.webChromeClient = webChromeClient.apply {
+ onProgressChanged = { newProgress -> progress = newProgress }
}
if (isReadOnly) {
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/CustomFieldModels.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/CustomFieldModels.kt
new file mode 100644
index 00000000000..4316a081c48
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/CustomFieldModels.kt
@@ -0,0 +1,61 @@
+package com.woocommerce.android.ui.customfields
+
+import android.os.Parcelable
+import androidx.core.util.PatternsCompat
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize
+import org.wordpress.android.fluxc.model.metadata.WCMetaData
+import org.wordpress.android.fluxc.model.metadata.WCMetaDataValue
+import org.wordpress.android.util.HtmlUtils
+import java.util.regex.Pattern
+
+typealias CustomField = WCMetaData
+
+@Parcelize
+data class CustomFieldUiModel(
+ val key: String,
+ val value: String,
+ val id: Long? = null,
+) : Parcelable {
+ constructor(customField: CustomField) : this(customField.key, customField.valueAsString, customField.id)
+
+ val valueStrippedHtml: String
+ get() = HtmlUtils.fastStripHtml(value)
+
+ @IgnoredOnParcel
+ val contentType: CustomFieldContentType = CustomFieldContentType.fromMetadataValue(value)
+
+ fun toDomainModel() = CustomField(
+ id = id ?: 0, // Use 0 for new custom fields
+ key = key,
+ value = WCMetaDataValue.StringValue(value) // Treat all updates as string values
+ )
+}
+
+enum class CustomFieldContentType {
+ TEXT,
+ URL,
+ EMAIL,
+ PHONE;
+
+ companion object {
+ fun fromMetadataValue(value: String): CustomFieldContentType {
+ return when {
+ PatternsCompat.WEB_URL.matcher(value).matches() -> URL
+ PatternsCompat.EMAIL_ADDRESS.matcher(value).matches() -> EMAIL
+ value.startsWith("tel://") || phonePattern.matcher(value).matches() -> PHONE
+ else -> TEXT
+ }
+ }
+
+ private val phonePattern by lazy {
+ // Copied from android.util.Patterns.PHONE to make it work with tests
+ Pattern.compile(
+ // sdd = space, dot, or dash
+ "(\\+[0-9]+[\\- .]*)?" + // +*
+ "(\\([0-9]+\\)[\\- .]*)?" + // ()*
+ "([0-9][0-9\\- .]+[0-9])" // +
+ )
+ }
+ }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/CustomFieldsRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/CustomFieldsRepository.kt
new file mode 100644
index 00000000000..1af466ddc5c
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/CustomFieldsRepository.kt
@@ -0,0 +1,67 @@
+package com.woocommerce.android.ui.customfields
+
+import com.woocommerce.android.WooException
+import com.woocommerce.android.tools.SelectedSite
+import com.woocommerce.android.util.WooLog
+import kotlinx.coroutines.flow.Flow
+import org.wordpress.android.fluxc.model.metadata.MetaDataParentItemType
+import org.wordpress.android.fluxc.model.metadata.UpdateMetadataRequest
+import org.wordpress.android.fluxc.store.MetaDataStore
+import javax.inject.Inject
+
+class CustomFieldsRepository @Inject constructor(
+ private val selectedSite: SelectedSite,
+ private val metaDataStore: MetaDataStore
+) {
+ fun observeDisplayableCustomFields(
+ parentItemId: Long,
+ ): Flow> = metaDataStore.observeDisplayableMetaData(selectedSite.get(), parentItemId)
+
+ suspend fun getDisplayableCustomFields(
+ parentItemId: Long,
+ ): List = metaDataStore.getDisplayableMetaData(selectedSite.get(), parentItemId)
+
+ suspend fun refreshCustomFields(
+ parentItemId: Long,
+ parentItemType: MetaDataParentItemType
+ ): Result {
+ return metaDataStore.refreshMetaData(
+ site = selectedSite.get(),
+ parentItemId = parentItemId,
+ parentItemType = parentItemType
+ ).let {
+ if (it.isError) {
+ WooLog.w(WooLog.T.CUSTOM_FIELDS, "Failed to refresh custom fields: ${it.error}")
+ Result.failure(WooException(it.error))
+ } else {
+ WooLog.d(WooLog.T.CUSTOM_FIELDS, "Successfully refreshed custom fields")
+ Result.success(Unit)
+ }
+ }
+ }
+
+ suspend fun updateCustomFields(request: UpdateMetadataRequest): Result {
+ return metaDataStore.updateMetaData(selectedSite.get(), request).let {
+ if (it.isError) {
+ WooLog.w(WooLog.T.CUSTOM_FIELDS, "Failed to update custom fields: ${it.error}")
+ Result.failure(WooException(it.error))
+ } else {
+ WooLog.d(WooLog.T.CUSTOM_FIELDS, "Successfully updated custom fields")
+ Result.success(Unit)
+ }
+ }
+ }
+
+ suspend fun hasDisplayableCustomFields(
+ parentItemId: Long,
+ ) = metaDataStore.hasDisplayableMetaData(selectedSite.get(), parentItemId)
+
+ suspend fun getCustomFieldById(
+ parentItemId: Long,
+ customFieldId: Long,
+ ): CustomField? = metaDataStore.getMetaDataById(
+ site = selectedSite.get(),
+ parentItemId = parentItemId,
+ metaDataId = customFieldId
+ )
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorFragment.kt
new file mode 100644
index 00000000000..d871b51c30a
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorFragment.kt
@@ -0,0 +1,49 @@
+package com.woocommerce.android.ui.customfields.editor
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import com.woocommerce.android.R
+import com.woocommerce.android.extensions.copyToClipboard
+import com.woocommerce.android.extensions.navigateBackWithResult
+import com.woocommerce.android.ui.base.BaseFragment
+import com.woocommerce.android.ui.compose.composeView
+import com.woocommerce.android.ui.main.AppBarStatus
+import com.woocommerce.android.viewmodel.MultiLiveEvent
+import dagger.hilt.android.AndroidEntryPoint
+import org.wordpress.android.util.ToastUtils
+
+@AndroidEntryPoint
+class CustomFieldsEditorFragment : BaseFragment() {
+ override val activityAppBarStatus: AppBarStatus = AppBarStatus.Hidden
+
+ private val viewModel: CustomFieldsEditorViewModel by viewModels()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ return composeView {
+ CustomFieldsEditorScreen(viewModel)
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ handleEvents()
+ }
+
+ private fun handleEvents() {
+ viewModel.event.observe(viewLifecycleOwner) { event ->
+ when (event) {
+ is CustomFieldsEditorViewModel.CopyContentToClipboard -> copyToClipboard(event)
+ is MultiLiveEvent.Event.ExitWithResult<*> -> navigateBackWithResult(event.key!!, event.data)
+ MultiLiveEvent.Event.Exit -> findNavController().navigateUp()
+ }
+ }
+ }
+
+ private fun copyToClipboard(event: CustomFieldsEditorViewModel.CopyContentToClipboard) {
+ requireContext().copyToClipboard(getString(event.labelResource), event.content)
+ ToastUtils.showToast(requireContext(), R.string.copied_to_clipboard)
+ }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorScreen.kt
new file mode 100644
index 00000000000..1670091a063
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorScreen.kt
@@ -0,0 +1,156 @@
+package com.woocommerce.android.ui.customfields.editor
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.woocommerce.android.R
+import com.woocommerce.android.ui.compose.component.DiscardChangesDialog
+import com.woocommerce.android.ui.compose.component.Toolbar
+import com.woocommerce.android.ui.compose.component.WCOutlinedTextField
+import com.woocommerce.android.ui.compose.component.WCOverflowMenu
+import com.woocommerce.android.ui.compose.component.WCTextButton
+import com.woocommerce.android.ui.compose.component.aztec.OutlinedAztecEditor
+import com.woocommerce.android.ui.compose.component.getText
+import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews
+import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground
+import com.woocommerce.android.ui.customfields.CustomFieldUiModel
+
+@Composable
+fun CustomFieldsEditorScreen(viewModel: CustomFieldsEditorViewModel) {
+ viewModel.state.observeAsState().value?.let { state ->
+ CustomFieldsEditorScreen(
+ state = state,
+ onKeyChanged = viewModel::onKeyChanged,
+ onValueChanged = viewModel::onValueChanged,
+ onDoneClicked = viewModel::onDoneClicked,
+ onDeleteClicked = viewModel::onDeleteClicked,
+ onCopyKeyClicked = viewModel::onCopyKeyClicked,
+ onCopyValueClicked = viewModel::onCopyValueClicked,
+ onBackButtonClick = viewModel::onBackClick,
+ )
+ }
+}
+
+@Composable
+private fun CustomFieldsEditorScreen(
+ state: CustomFieldsEditorViewModel.UiState,
+ onKeyChanged: (String) -> Unit,
+ onValueChanged: (String) -> Unit,
+ onDoneClicked: () -> Unit,
+ onDeleteClicked: () -> Unit,
+ onCopyKeyClicked: () -> Unit,
+ onCopyValueClicked: () -> Unit,
+ onBackButtonClick: () -> Unit,
+) {
+ BackHandler { onBackButtonClick() }
+
+ Scaffold(
+ topBar = {
+ Toolbar(
+ title = "Custom Field",
+ onNavigationButtonClick = onBackButtonClick,
+ actions = {
+ if (state.showDoneButton) {
+ WCTextButton(
+ onClick = onDoneClicked,
+ text = stringResource(R.string.done)
+ )
+ }
+ WCOverflowMenu(
+ items = listOfNotNull(
+ R.string.custom_fields_editor_copy_key,
+ R.string.custom_fields_editor_copy_value,
+ if (!state.isCreatingNewItem) R.string.delete else null,
+ ),
+ mapper = { stringResource(it) },
+ itemColor = {
+ when (it) {
+ R.string.delete -> MaterialTheme.colors.error
+ else -> LocalContentColor.current
+ }
+ },
+ onSelected = { resourceId ->
+ when (resourceId) {
+ R.string.delete -> onDeleteClicked()
+ R.string.custom_fields_editor_copy_key -> onCopyKeyClicked()
+ R.string.custom_fields_editor_copy_value -> onCopyValueClicked()
+ else -> error("Unhandled menu item")
+ }
+ }
+ )
+ }
+ )
+ },
+ backgroundColor = MaterialTheme.colors.surface
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .padding(paddingValues)
+ .padding(16.dp)
+ ) {
+ WCOutlinedTextField(
+ value = state.customField.key,
+ onValueChange = onKeyChanged,
+ label = stringResource(R.string.custom_fields_editor_key_label),
+ helperText = state.keyErrorMessage?.getText(),
+ isError = state.keyErrorMessage != null,
+ singleLine = true
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ if (state.isHtml) {
+ OutlinedAztecEditor(
+ content = state.customField.value,
+ onContentChanged = onValueChanged,
+ label = stringResource(R.string.custom_fields_editor_value_label),
+ minLines = 5
+ )
+ } else {
+ WCOutlinedTextField(
+ value = state.customField.value,
+ onValueChange = onValueChanged,
+ label = stringResource(R.string.custom_fields_editor_value_label),
+ minLines = 5
+ )
+ }
+ }
+
+ state.discardChangesDialogState?.let {
+ DiscardChangesDialog(
+ discardButton = it.onDiscard,
+ dismissButton = it.onCancel
+ )
+ }
+ }
+}
+
+@LightDarkThemePreviews
+@Composable
+private fun CustomFieldsEditorScreenPreview() {
+ WooThemeWithBackground {
+ CustomFieldsEditorScreen(
+ CustomFieldsEditorViewModel.UiState(customField = CustomFieldUiModel("key", "value")),
+ onKeyChanged = {},
+ onValueChanged = {},
+ onDoneClicked = {},
+ onDeleteClicked = {},
+ onCopyKeyClicked = {},
+ onCopyValueClicked = {},
+ onBackButtonClick = {}
+ )
+ }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorViewModel.kt
new file mode 100644
index 00000000000..c2172cb9d29
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorViewModel.kt
@@ -0,0 +1,172 @@
+package com.woocommerce.android.ui.customfields.editor
+
+import android.os.Parcelable
+import androidx.annotation.StringRes
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.asLiveData
+import androidx.lifecycle.viewModelScope
+import com.woocommerce.android.R
+import com.woocommerce.android.model.UiString
+import com.woocommerce.android.ui.customfields.CustomFieldUiModel
+import com.woocommerce.android.ui.customfields.CustomFieldsRepository
+import com.woocommerce.android.viewmodel.MultiLiveEvent
+import com.woocommerce.android.viewmodel.ScopedViewModel
+import com.woocommerce.android.viewmodel.getNullableStateFlow
+import com.woocommerce.android.viewmodel.getStateFlow
+import com.woocommerce.android.viewmodel.navArgs
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.parcelize.Parcelize
+import javax.inject.Inject
+
+@HiltViewModel
+class CustomFieldsEditorViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val repository: CustomFieldsRepository
+) : ScopedViewModel(savedStateHandle) {
+ companion object {
+ const val CUSTOM_FIELD_CREATED_RESULT_KEY = "custom_field_created"
+ const val CUSTOM_FIELD_UPDATED_RESULT_KEY = "custom_field_updated"
+ const val CUSTOM_FIELD_DELETED_RESULT_KEY = "custom_field_deleted"
+ }
+
+ private val navArgs by savedStateHandle.navArgs()
+
+ private val customFieldDraft = savedStateHandle.getStateFlow(
+ scope = viewModelScope,
+ initialValue = navArgs.customField ?: CustomFieldUiModel("", ""),
+ key = "customFieldDraft"
+ )
+ private val showDiscardChangesDialog = savedStateHandle.getStateFlow(
+ scope = viewModelScope,
+ initialValue = false,
+ key = "showDiscardChangesDialog"
+ )
+ private val keyErrorMessage = savedStateHandle.getNullableStateFlow(
+ scope = viewModelScope,
+ initialValue = null,
+ clazz = UiString::class.java,
+ key = "keyErrorMessage"
+ )
+ private val storedValue = navArgs.customField
+ private val isHtml = storedValue?.valueStrippedHtml != storedValue?.value
+
+ val state = combine(
+ customFieldDraft,
+ showDiscardChangesDialog.mapToState(),
+ keyErrorMessage
+ ) { customField, discardChangesDialogState, keyErrorMessage ->
+ UiState(
+ customField = customField,
+ hasChanges = storedValue?.key.orEmpty() != customField.key ||
+ storedValue?.value.orEmpty() != customField.value,
+ isHtml = isHtml,
+ discardChangesDialogState = discardChangesDialogState,
+ keyErrorMessage = keyErrorMessage,
+ isCreatingNewItem = storedValue == null
+ )
+ }.asLiveData()
+
+ fun onKeyChanged(key: String) {
+ keyErrorMessage.value = if (key.startsWith("_")) {
+ UiString.UiStringRes(R.string.custom_fields_editor_key_error_underscore)
+ } else {
+ null
+ }
+ customFieldDraft.update { it.copy(key = key) }
+ }
+
+ fun onValueChanged(value: String) {
+ customFieldDraft.update { it.copy(value = value) }
+ }
+
+ fun onDoneClicked() {
+ launch {
+ val value = requireNotNull(customFieldDraft.value)
+ if (value.id == null) {
+ // Check for duplicate keys before inserting the new custom field
+ // For more context: pe5sF9-33t-p2#comment-3880
+ val existingFields = repository.getDisplayableCustomFields(navArgs.parentItemId)
+ if (existingFields.any { it.key == value.key }) {
+ keyErrorMessage.value = UiString.UiStringRes(R.string.custom_fields_editor_key_error_duplicate)
+ return@launch
+ }
+ }
+
+ val event = if (storedValue == null) {
+ MultiLiveEvent.Event.ExitWithResult(data = value, key = CUSTOM_FIELD_CREATED_RESULT_KEY)
+ } else {
+ MultiLiveEvent.Event.ExitWithResult(
+ data = CustomFieldUpdateResult(oldKey = storedValue.key, updatedField = value),
+ key = CUSTOM_FIELD_UPDATED_RESULT_KEY
+ )
+ }
+ triggerEvent(event)
+ }
+ }
+
+ fun onDeleteClicked() {
+ triggerEvent(
+ MultiLiveEvent.Event.ExitWithResult(data = navArgs.customField, key = CUSTOM_FIELD_DELETED_RESULT_KEY)
+ )
+ }
+
+ fun onCopyKeyClicked() {
+ triggerEvent(CopyContentToClipboard(R.string.custom_fields_editor_key_label, customFieldDraft.value.key))
+ }
+
+ fun onCopyValueClicked() {
+ triggerEvent(CopyContentToClipboard(R.string.custom_fields_editor_value_label, customFieldDraft.value.value))
+ }
+
+ fun onBackClick() {
+ if (state.value?.hasChanges == true) {
+ showDiscardChangesDialog.value = true
+ } else {
+ triggerEvent(MultiLiveEvent.Event.Exit)
+ }
+ }
+
+ private fun Flow.mapToState() = map {
+ if (it) {
+ DiscardChangesDialogState(
+ onDiscard = { triggerEvent(MultiLiveEvent.Event.Exit) },
+ onCancel = { showDiscardChangesDialog.value = false }
+ )
+ } else {
+ null
+ }
+ }
+
+ data class UiState(
+ val customField: CustomFieldUiModel = CustomFieldUiModel("", ""),
+ val hasChanges: Boolean = false,
+ val isHtml: Boolean = false,
+ val discardChangesDialogState: DiscardChangesDialogState? = null,
+ val keyErrorMessage: UiString? = null,
+ val isCreatingNewItem: Boolean = false
+ ) {
+ val showDoneButton
+ get() = customField.key.isNotEmpty() && hasChanges && keyErrorMessage == null
+ }
+
+ data class DiscardChangesDialogState(
+ val onDiscard: () -> Unit,
+ val onCancel: () -> Unit
+ )
+
+ data class CopyContentToClipboard(
+ @StringRes val labelResource: Int,
+ val content: String
+ ) : MultiLiveEvent.Event()
+
+ @Parcelize
+ data class CustomFieldUpdateResult(
+ val oldKey: String,
+ val updatedField: CustomFieldUiModel
+ ) : Parcelable
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsFragment.kt
new file mode 100644
index 00000000000..cb52225db4f
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsFragment.kt
@@ -0,0 +1,109 @@
+package com.woocommerce.android.ui.customfields.list
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.material.SnackbarHostState
+import androidx.compose.material.SnackbarResult
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import com.woocommerce.android.extensions.handleResult
+import com.woocommerce.android.ui.base.BaseFragment
+import com.woocommerce.android.ui.compose.composeView
+import com.woocommerce.android.ui.customfields.CustomFieldContentType
+import com.woocommerce.android.ui.customfields.CustomFieldUiModel
+import com.woocommerce.android.ui.customfields.editor.CustomFieldsEditorViewModel
+import com.woocommerce.android.ui.customfields.editor.CustomFieldsEditorViewModel.CustomFieldUpdateResult
+import com.woocommerce.android.ui.main.AppBarStatus
+import com.woocommerce.android.util.ActivityUtils
+import com.woocommerce.android.util.ChromeCustomTabUtils
+import com.woocommerce.android.viewmodel.MultiLiveEvent
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+
+@AndroidEntryPoint
+class CustomFieldsFragment : BaseFragment() {
+ private val viewModel: CustomFieldsViewModel by viewModels()
+
+ private val snackbarHostState = SnackbarHostState()
+
+ override val activityAppBarStatus = AppBarStatus.Hidden
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ return composeView {
+ CustomFieldsScreen(
+ viewModel = viewModel,
+ snackbarHostState = snackbarHostState
+ )
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ handleEvents()
+ handleResults()
+ }
+
+ private fun handleEvents() {
+ viewModel.event.observe(viewLifecycleOwner) { event ->
+ when (event) {
+ is CustomFieldsViewModel.OpenCustomFieldEditor -> openEditor(event.field)
+ is CustomFieldsViewModel.CustomFieldValueClicked -> handleValueClick(event.field)
+ is MultiLiveEvent.Event.ShowSnackbar -> showSnackbar(getString(event.message))
+ is MultiLiveEvent.Event.ShowActionSnackbar -> showSnackbar(
+ message = event.message,
+ actionText = event.actionText,
+ action = { event.action.onClick(null) }
+ )
+
+ is MultiLiveEvent.Event.Exit -> {
+ findNavController().navigateUp()
+ }
+ }
+ }
+ }
+
+ private fun handleResults() {
+ handleResult(CustomFieldsEditorViewModel.CUSTOM_FIELD_CREATED_RESULT_KEY) { result ->
+ viewModel.onCustomFieldInserted(result)
+ }
+ handleResult(CustomFieldsEditorViewModel.CUSTOM_FIELD_UPDATED_RESULT_KEY) { result ->
+ viewModel.onCustomFieldUpdated(result.oldKey, result.updatedField)
+ }
+ handleResult(CustomFieldsEditorViewModel.CUSTOM_FIELD_DELETED_RESULT_KEY) { result ->
+ viewModel.onCustomFieldDeleted(result)
+ }
+ }
+
+ private fun openEditor(field: CustomFieldUiModel?) {
+ findNavController().navigate(
+ CustomFieldsFragmentDirections.actionCustomFieldsFragmentToCustomFieldsEditorFragment(
+ parentItemId = viewModel.parentItemId,
+ customField = field
+ )
+ )
+ }
+
+ private fun handleValueClick(field: CustomFieldUiModel) {
+ when (field.contentType) {
+ CustomFieldContentType.URL -> ChromeCustomTabUtils.launchUrl(requireContext(), field.value)
+ CustomFieldContentType.EMAIL -> ActivityUtils.sendEmail(requireContext(), field.value)
+ CustomFieldContentType.PHONE -> ActivityUtils.dialPhoneNumber(requireContext(), field.value)
+ CustomFieldContentType.TEXT -> error("Values of type TEXT should not be clickable")
+ }
+ }
+
+ private fun showSnackbar(
+ message: String,
+ actionText: String? = null,
+ action: (() -> Unit)? = null
+ ) {
+ viewLifecycleOwner.lifecycleScope.launch {
+ val result = snackbarHostState.showSnackbar(message = message, actionLabel = actionText)
+ if (actionText != null && action != null && result == SnackbarResult.ActionPerformed) {
+ action()
+ }
+ }
+ }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsScreen.kt
new file mode 100644
index 00000000000..afc35841ae0
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsScreen.kt
@@ -0,0 +1,276 @@
+package com.woocommerce.android.ui.customfields.list
+
+import android.content.res.Configuration
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.clickable
+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.fillMaxSize
+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.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material.Divider
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.SnackbarHost
+import androidx.compose.material.SnackbarHostState
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.pullrefresh.PullRefreshIndicator
+import androidx.compose.material.pullrefresh.pullRefresh
+import androidx.compose.material.pullrefresh.rememberPullRefreshState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.UrlAnnotation
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.woocommerce.android.R
+import com.woocommerce.android.ui.compose.component.DiscardChangesDialog
+import com.woocommerce.android.ui.compose.component.ExpandableTopBanner
+import com.woocommerce.android.ui.compose.component.ProgressDialog
+import com.woocommerce.android.ui.compose.component.Toolbar
+import com.woocommerce.android.ui.compose.component.WCTextButton
+import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews
+import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground
+import com.woocommerce.android.ui.customfields.CustomField
+import com.woocommerce.android.ui.customfields.CustomFieldContentType
+import com.woocommerce.android.ui.customfields.CustomFieldUiModel
+
+@Composable
+fun CustomFieldsScreen(
+ viewModel: CustomFieldsViewModel,
+ snackbarHostState: SnackbarHostState
+) {
+ viewModel.state.observeAsState().value?.let { state ->
+ CustomFieldsScreen(
+ state = state,
+ onPullToRefresh = viewModel::onPullToRefresh,
+ onSaveClicked = viewModel::onSaveClicked,
+ onCustomFieldClicked = viewModel::onCustomFieldClicked,
+ onCustomFieldValueClicked = viewModel::onCustomFieldValueClicked,
+ onAddCustomFieldClicked = viewModel::onAddCustomFieldClicked,
+ onBackClick = viewModel::onBackClick,
+ snackbarHostState = snackbarHostState
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+private fun CustomFieldsScreen(
+ state: CustomFieldsViewModel.UiState,
+ onPullToRefresh: () -> Unit,
+ onSaveClicked: () -> Unit,
+ onCustomFieldClicked: (CustomFieldUiModel) -> Unit,
+ onCustomFieldValueClicked: (CustomFieldUiModel) -> Unit,
+ onAddCustomFieldClicked: () -> Unit,
+ onBackClick: () -> Unit,
+ snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
+) {
+ BackHandler { onBackClick() }
+
+ Scaffold(
+ topBar = {
+ Toolbar(
+ title = stringResource(id = R.string.custom_fields_list_title),
+ onNavigationButtonClick = onBackClick,
+ actions = {
+ if (state.hasChanges) {
+ WCTextButton(
+ text = stringResource(id = R.string.save),
+ onClick = onSaveClicked
+ )
+ }
+ }
+ )
+ },
+ floatingActionButton = {
+ FloatingActionButton(
+ onClick = onAddCustomFieldClicked,
+ backgroundColor = MaterialTheme.colors.primary,
+ contentColor = Color.White
+ ) {
+ Icon(
+ imageVector = Icons.Default.Add,
+ contentDescription = stringResource(id = R.string.custom_fields_add_button)
+ )
+ }
+ },
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ backgroundColor = MaterialTheme.colors.surface
+ ) { paddingValues ->
+ val pullToRefreshState = rememberPullRefreshState(state.isRefreshing, onPullToRefresh)
+
+ Column(
+ modifier = Modifier
+ .padding(paddingValues)
+ ) {
+ if (state.topBannerState != null) {
+ ExpandableTopBanner(
+ title = stringResource(id = R.string.custom_fields_list_top_banner_title),
+ message = stringResource(id = R.string.custom_fields_list_top_banner_message),
+ onDismiss = state.topBannerState.onDismiss,
+ expandedByDefault = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ Box(
+ modifier = Modifier
+ .padding(paddingValues)
+ .pullRefresh(state = pullToRefreshState)
+ ) {
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(state.customFields) { customField ->
+ CustomFieldItem(
+ customField = customField,
+ onClicked = onCustomFieldClicked,
+ onValueClicked = onCustomFieldValueClicked,
+ modifier = Modifier.fillMaxWidth()
+ )
+ Divider()
+ }
+ }
+
+ PullRefreshIndicator(
+ refreshing = state.isRefreshing,
+ state = pullToRefreshState,
+ modifier = Modifier.align(Alignment.TopCenter)
+ )
+ }
+ }
+
+ if (state.isSaving) {
+ ProgressDialog(
+ title = stringResource(id = R.string.custom_fields_list_progress_dialog_title),
+ subtitle = stringResource(id = R.string.please_wait)
+ )
+ }
+
+ state.discardChangesDialogState?.let {
+ DiscardChangesDialog(
+ discardButton = it.onDiscard,
+ dismissButton = it.onCancel
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalTextApi::class)
+@Composable
+private fun CustomFieldItem(
+ customField: CustomFieldUiModel,
+ onClicked: (CustomFieldUiModel) -> Unit,
+ onValueClicked: (CustomFieldUiModel) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = modifier
+ .clickable(onClick = { onClicked(customField) })
+ .padding(16.dp)
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = customField.key,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.subtitle2
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+
+ if (customField.contentType != CustomFieldContentType.TEXT) {
+ val text = buildAnnotatedString {
+ withStyle(SpanStyle(color = MaterialTheme.colors.primary)) {
+ pushUrlAnnotation(UrlAnnotation(customField.value))
+ append(customField.value)
+ }
+ }
+ ClickableText(
+ text = text,
+ style = MaterialTheme.typography.body2.copy(
+ color = MaterialTheme.colors.onSurface
+ ),
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ onClick = { offset ->
+ text.getUrlAnnotations(
+ start = offset,
+ end = offset
+ ).firstOrNull()?.let { _ ->
+ onValueClicked(customField)
+ }
+ }
+ )
+ } else {
+ Text(
+ text = customField.value,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.body2
+ )
+ }
+ }
+
+ Icon(
+ imageVector = Icons.AutoMirrored.Default.ArrowForwardIos,
+ contentDescription = null,
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .size(16.dp)
+ )
+ }
+}
+
+@LightDarkThemePreviews
+@Preview
+@Composable
+private fun CustomFieldsScreenPreview() {
+ WooThemeWithBackground {
+ CustomFieldsScreen(
+ state = CustomFieldsViewModel.UiState(
+ customFields = listOf(
+ CustomFieldUiModel(CustomField(0, "key1", "Value 1")),
+ CustomFieldUiModel(CustomField(1, "key2", "Value 2")),
+ CustomFieldUiModel(
+ CustomField(
+ id = 2,
+ key = "key3",
+ value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
+ "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
+ )
+ ),
+ CustomFieldUiModel(CustomField(3, "key4", "https://url.com")),
+ ),
+ topBannerState = CustomFieldsViewModel.TopBannerState { }
+ ),
+ onPullToRefresh = {},
+ onSaveClicked = {},
+ onCustomFieldClicked = {},
+ onCustomFieldValueClicked = {},
+ onAddCustomFieldClicked = {},
+ onBackClick = {}
+ )
+ }
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsViewModel.kt
new file mode 100644
index 00000000000..c9dc9e8653f
--- /dev/null
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsViewModel.kt
@@ -0,0 +1,216 @@
+package com.woocommerce.android.ui.customfields.list
+
+import android.os.Parcelable
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.asLiveData
+import androidx.lifecycle.viewModelScope
+import com.woocommerce.android.AppPrefsWrapper
+import com.woocommerce.android.R
+import com.woocommerce.android.extensions.combine
+import com.woocommerce.android.ui.customfields.CustomFieldUiModel
+import com.woocommerce.android.ui.customfields.CustomFieldsRepository
+import com.woocommerce.android.viewmodel.MultiLiveEvent
+import com.woocommerce.android.viewmodel.ResourceProvider
+import com.woocommerce.android.viewmodel.ScopedViewModel
+import com.woocommerce.android.viewmodel.getStateFlow
+import com.woocommerce.android.viewmodel.navArgs
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.parcelize.Parcelize
+import org.wordpress.android.fluxc.model.metadata.UpdateMetadataRequest
+import javax.inject.Inject
+
+@HiltViewModel
+class CustomFieldsViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val repository: CustomFieldsRepository,
+ private val appPrefs: AppPrefsWrapper,
+ private val resourceProvider: ResourceProvider
+) : ScopedViewModel(savedStateHandle) {
+ private val args: CustomFieldsFragmentArgs by savedStateHandle.navArgs()
+ val parentItemId: Long = args.parentItemId
+
+ private val isRefreshing = MutableStateFlow(false)
+ private val isSaving = MutableStateFlow(false)
+ private val showDiscardChangesDialog = savedStateHandle.getStateFlow(
+ scope = viewModelScope,
+ initialValue = false,
+ key = "showDiscardChangesDialog"
+ )
+ private val customFields = repository.observeDisplayableCustomFields(args.parentItemId)
+ private val pendingChanges = savedStateHandle.getStateFlow(viewModelScope, PendingChanges())
+ private val bannerDismissed = appPrefs.observePrefs()
+ .onStart { emit(Unit) }
+ .map { appPrefs.isCustomFieldsTopBannerDismissed }
+ .distinctUntilChanged()
+
+ val state = combine(
+ customFields,
+ pendingChanges,
+ isRefreshing,
+ isSaving,
+ showDiscardChangesDialog,
+ bannerDismissed
+ ) { customFields, pendingChanges, isLoading, isSaving, isShowingDiscardDialog, bannerDismissed ->
+ UiState(
+ customFields = customFields.map { CustomFieldUiModel(it) }.combineWithChanges(pendingChanges),
+ isRefreshing = isLoading,
+ isSaving = isSaving,
+ hasChanges = pendingChanges.hasChanges,
+ discardChangesDialogState = isShowingDiscardDialog.takeIf { it }?.let {
+ DiscardChangesDialogState(
+ onDiscard = { triggerEvent(MultiLiveEvent.Event.Exit) },
+ onCancel = { showDiscardChangesDialog.value = false }
+ )
+ },
+ topBannerState = bannerDismissed.takeIf { !it }?.let {
+ TopBannerState {
+ appPrefs.isCustomFieldsTopBannerDismissed = true
+ }
+ }
+ )
+ }.asLiveData()
+
+ fun onBackClick() {
+ if (pendingChanges.value.hasChanges) {
+ showDiscardChangesDialog.value = true
+ } else {
+ triggerEvent(MultiLiveEvent.Event.Exit)
+ }
+ }
+
+ fun onPullToRefresh() {
+ launch {
+ isRefreshing.value = true
+ repository.refreshCustomFields(args.parentItemId, args.parentItemType).onFailure {
+ triggerEvent(MultiLiveEvent.Event.ShowSnackbar(R.string.custom_fields_list_loading_error))
+ }
+ isRefreshing.value = false
+ }
+ }
+
+ fun onCustomFieldClicked(field: CustomFieldUiModel) {
+ triggerEvent(OpenCustomFieldEditor(field))
+ }
+
+ fun onCustomFieldValueClicked(field: CustomFieldUiModel) {
+ triggerEvent(CustomFieldValueClicked(field))
+ }
+
+ fun onAddCustomFieldClicked() {
+ triggerEvent(OpenCustomFieldEditor(null))
+ }
+
+ fun onCustomFieldInserted(result: CustomFieldUiModel) {
+ pendingChanges.update {
+ it.copy(insertedFields = it.insertedFields + result)
+ }
+ }
+
+ fun onCustomFieldUpdated(oldValueKey: String, result: CustomFieldUiModel) {
+ pendingChanges.update {
+ if (result.id == null) {
+ // We are updating a field that was just added and hasn't been saved yet
+ it.copy(insertedFields = it.insertedFields.filterNot { field -> field.key == oldValueKey } + result)
+ } else {
+ it.copy(editedFields = it.editedFields.filterNot { field -> field.id == result.id } + result)
+ }
+ }
+ }
+
+ fun onCustomFieldDeleted(field: CustomFieldUiModel) {
+ pendingChanges.update {
+ if (field.id == null) {
+ // This field was just added and hasn't been saved yet
+ it.copy(insertedFields = it.insertedFields - field)
+ } else {
+ it.copy(deletedFieldIds = it.deletedFieldIds + field.id)
+ }
+ }
+
+ triggerEvent(
+ MultiLiveEvent.Event.ShowActionSnackbar(
+ message = resourceProvider.getString(R.string.custom_fields_list_field_deleted),
+ actionText = resourceProvider.getString(R.string.undo),
+ action = {
+ pendingChanges.update {
+ if (field.id == null) {
+ it.copy(insertedFields = it.insertedFields + field)
+ } else {
+ it.copy(deletedFieldIds = it.deletedFieldIds - field.id)
+ }
+ }
+ }
+ )
+ )
+ }
+
+ fun onSaveClicked() {
+ launch {
+ isSaving.value = true
+ val currentPendingChanges = pendingChanges.value
+ val request = UpdateMetadataRequest(
+ parentItemId = args.parentItemId,
+ parentItemType = args.parentItemType,
+ updatedMetadata = currentPendingChanges.editedFields.map { it.toDomainModel() },
+ insertedMetadata = currentPendingChanges.insertedFields.map { it.toDomainModel() },
+ deletedMetadataIds = currentPendingChanges.deletedFieldIds
+ )
+
+ repository.updateCustomFields(request)
+ .fold(
+ onSuccess = {
+ pendingChanges.value = PendingChanges()
+ triggerEvent(MultiLiveEvent.Event.ShowSnackbar(R.string.custom_fields_list_saving_succeeded))
+ },
+ onFailure = {
+ triggerEvent(MultiLiveEvent.Event.ShowSnackbar(R.string.custom_fields_list_saving_failed))
+ }
+ )
+ isSaving.value = false
+ }
+ }
+
+ private fun List.combineWithChanges(pendingChanges: PendingChanges) =
+ filterNot { it.id in pendingChanges.deletedFieldIds }
+ .map { customField ->
+ pendingChanges.editedFields.find { it.id == customField.id } ?: customField
+ }
+ .plus(pendingChanges.insertedFields)
+
+ data class UiState(
+ val customFields: List,
+ val isRefreshing: Boolean = false,
+ val isSaving: Boolean = false,
+ val hasChanges: Boolean = false,
+ val discardChangesDialogState: DiscardChangesDialogState? = null,
+ val topBannerState: TopBannerState? = null
+ )
+
+ data class DiscardChangesDialogState(
+ val onDiscard: () -> Unit,
+ val onCancel: () -> Unit
+ )
+
+ data class TopBannerState(
+ val onDismiss: () -> Unit
+ )
+
+ @Parcelize
+ private data class PendingChanges(
+ val editedFields: List = emptyList(),
+ val insertedFields: List = emptyList(),
+ val deletedFieldIds: List = emptyList()
+ ) : Parcelable {
+ val hasChanges: Boolean
+ get() = editedFields.isNotEmpty() || insertedFields.isNotEmpty() || deletedFieldIds.isNotEmpty()
+ }
+
+ data class OpenCustomFieldEditor(val field: CustomFieldUiModel?) : MultiLiveEvent.Event()
+ data class CustomFieldValueClicked(val field: CustomFieldUiModel) : MultiLiveEvent.Event()
+}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/blaze/DashboardBlazeCard.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/blaze/DashboardBlazeCard.kt
index 81406411386..7397e6f4904 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/blaze/DashboardBlazeCard.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/blaze/DashboardBlazeCard.kt
@@ -33,7 +33,6 @@ import com.woocommerce.android.R
import com.woocommerce.android.R.string
import com.woocommerce.android.extensions.navigateSafely
import com.woocommerce.android.model.DashboardWidget
-import com.woocommerce.android.ui.blaze.BlazeCampaignStat
import com.woocommerce.android.ui.blaze.BlazeCampaignUi
import com.woocommerce.android.ui.blaze.BlazeProductUi
import com.woocommerce.android.ui.blaze.BlazeUrlsHelper.BlazeFlowSource
@@ -348,20 +347,11 @@ fun MyStoreBlazeViewCampaignPreview() {
campaign = BlazeCampaignUi(
product = product,
status = CampaignStatusUi.Active,
- stats = listOf(
- BlazeCampaignStat(
- name = string.blaze_campaign_status_impressions,
- value = 100.toString()
- ),
- BlazeCampaignStat(
- name = string.blaze_campaign_status_clicks,
- value = 10.toString()
- ),
- BlazeCampaignStat(
- name = string.blaze_campaign_status_budget,
- value = 1000.toString()
- ),
- ),
+ isEndlessCampaign = false,
+ impressions = "32435",
+ clicks = "43",
+ formattedBudget = "$100",
+ budgetLabel = R.string.blaze_campaign_status_budget_total
),
onCampaignClicked = {},
onCreateCampaignClicked = {},
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/blaze/DashboardBlazeViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/blaze/DashboardBlazeViewModel.kt
index 4c5e361710c..e9c828d3d44 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/blaze/DashboardBlazeViewModel.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/blaze/DashboardBlazeViewModel.kt
@@ -9,15 +9,15 @@ import com.woocommerce.android.analytics.AnalyticsEvent.BLAZE_CAMPAIGN_LIST_ENTR
import com.woocommerce.android.analytics.AnalyticsEvent.BLAZE_ENTRY_POINT_DISPLAYED
import com.woocommerce.android.analytics.AnalyticsTracker
import com.woocommerce.android.analytics.AnalyticsTrackerWrapper
+import com.woocommerce.android.extensions.NumberExtensionsWrapper
import com.woocommerce.android.model.DashboardWidget
import com.woocommerce.android.model.Product
-import com.woocommerce.android.ui.blaze.BlazeCampaignStat
import com.woocommerce.android.ui.blaze.BlazeCampaignUi
import com.woocommerce.android.ui.blaze.BlazeProductUi
import com.woocommerce.android.ui.blaze.BlazeUrlsHelper
import com.woocommerce.android.ui.blaze.BlazeUrlsHelper.BlazeFlowSource.MY_STORE_SECTION
-import com.woocommerce.android.ui.blaze.CampaignStatusUi
import com.woocommerce.android.ui.blaze.ObserveMostRecentBlazeCampaign
+import com.woocommerce.android.ui.blaze.toUiState
import com.woocommerce.android.ui.dashboard.DashboardViewModel
import com.woocommerce.android.ui.dashboard.DashboardViewModel.DashboardWidgetAction
import com.woocommerce.android.ui.dashboard.DashboardViewModel.DashboardWidgetMenu
@@ -27,6 +27,7 @@ import com.woocommerce.android.ui.dashboard.blaze.DashboardBlazeViewModel.Dashbo
import com.woocommerce.android.ui.dashboard.defaultHideMenuEntry
import com.woocommerce.android.ui.products.ProductStatus
import com.woocommerce.android.ui.products.list.ProductListRepository
+import com.woocommerce.android.util.CurrencyFormatter
import com.woocommerce.android.viewmodel.MultiLiveEvent
import com.woocommerce.android.viewmodel.ScopedViewModel
import dagger.assisted.Assisted
@@ -55,7 +56,9 @@ class DashboardBlazeViewModel @AssistedInject constructor(
observeMostRecentBlazeCampaign: ObserveMostRecentBlazeCampaign,
private val productListRepository: ProductListRepository,
private val blazeUrlsHelper: BlazeUrlsHelper,
- private val analyticsTrackerWrapper: AnalyticsTrackerWrapper
+ private val analyticsTrackerWrapper: AnalyticsTrackerWrapper,
+ private val currencyFormatter: CurrencyFormatter,
+ private val numberExtensionsWrapper: NumberExtensionsWrapper
) : ScopedViewModel(savedStateHandle) {
private val _refreshTrigger = MutableSharedFlow(extraBufferCapacity = 1)
private val refreshTrigger = merge(_refreshTrigger, (parentViewModel.refreshTrigger))
@@ -141,23 +144,7 @@ class DashboardBlazeViewModel @AssistedInject constructor(
private fun showUiForCampaign(campaign: BlazeCampaignModel): DashboardBlazeCampaignState {
return Campaign(
- campaign = BlazeCampaignUi(
- product = BlazeProductUi(
- name = campaign.title,
- imgUrl = campaign.imageUrl.orEmpty(),
- ),
- status = CampaignStatusUi.fromString(campaign.uiStatus),
- stats = listOf(
- BlazeCampaignStat(
- name = string.blaze_campaign_status_impressions,
- value = campaign.impressions.toString()
- ),
- BlazeCampaignStat(
- name = string.blaze_campaign_status_clicks,
- value = campaign.clicks.toString()
- )
- )
- ),
+ campaign = campaign.toUiState(currencyFormatter, numberExtensionsWrapper),
onCampaignClicked = {
parentViewModel.trackCardInteracted(DashboardWidget.Type.BLAZE.trackingIdentifier)
analyticsTrackerWrapper.track(
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/DashboardDataStore.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/DashboardDataStore.kt
index ca33c920ea7..fe54b89941f 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/DashboardDataStore.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/data/DashboardDataStore.kt
@@ -58,7 +58,8 @@ class DashboardDataStore @Inject constructor(
this == DashboardWidget.Type.STATS ||
this == DashboardWidget.Type.POPULAR_PRODUCTS ||
this == DashboardWidget.Type.ONBOARDING ||
- this == DashboardWidget.Type.BLAZE
+ this == DashboardWidget.Type.BLAZE ||
+ this == DashboardWidget.Type.GOOGLE_ADS
return DashboardWidget.Type.supportedWidgets.map {
DashboardWidgetDataModel.newBuilder()
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/stats/DashboardStatsView.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/stats/DashboardStatsView.kt
index 28a514da900..4903eaaa88f 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/stats/DashboardStatsView.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/stats/DashboardStatsView.kt
@@ -552,6 +552,7 @@ class DashboardStatsView @JvmOverloads constructor(
fadeInLabelValue(ordersValue, orders)
if (chartRevenueStats.isEmpty() || revenueStatsModel?.totalSales == 0.toDouble()) {
+ binding.chart.clear()
isRequestingStats = false
return
}
@@ -648,8 +649,7 @@ class DashboardStatsView @JvmOverloads constructor(
}
private fun generateLineDataSet(revenueStats: Map): LineDataSet {
- chartRevenueStats = revenueStats
- val entries = chartRevenueStats.values.mapIndexed { index, value ->
+ val entries = revenueStats.values.mapIndexed { index, value ->
Entry((index + 1).toFloat(), value.toFloat())
}
return LineDataSet(entries, "")
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/google/webview/GoogleAdsWebViewScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/google/webview/GoogleAdsWebViewScreen.kt
index c80deb2016f..5720ba9d669 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/google/webview/GoogleAdsWebViewScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/google/webview/GoogleAdsWebViewScreen.kt
@@ -13,7 +13,7 @@ import androidx.compose.ui.res.stringResource
import com.woocommerce.android.R
import com.woocommerce.android.ui.common.wpcomwebview.WPComWebViewAuthenticator
import com.woocommerce.android.ui.compose.component.Toolbar
-import com.woocommerce.android.ui.compose.component.WCWebView
+import com.woocommerce.android.ui.compose.component.web.WCWebView
import com.woocommerce.android.ui.google.webview.GoogleAdsWebViewViewModel.DisplayMode.MODAL
import com.woocommerce.android.ui.google.webview.GoogleAdsWebViewViewModel.DisplayMode.REGULAR
import org.wordpress.android.fluxc.network.UserAgent
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jetpack/JetpackCPInstallViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jetpack/JetpackCPInstallViewModel.kt
index fbe2f1289ef..ecdec05ce9c 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jetpack/JetpackCPInstallViewModel.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jetpack/JetpackCPInstallViewModel.kt
@@ -24,7 +24,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
-import org.wordpress.android.fluxc.store.WooCommerceStore
+import org.wordpress.android.fluxc.store.SiteStore
import javax.inject.Inject
@HiltViewModel
@@ -32,7 +32,7 @@ class JetpackCPInstallViewModel @Inject constructor(
savedState: SavedStateHandle,
private val repository: PluginRepository,
private val selectedSite: SelectedSite,
- private val wooCommerceStore: WooCommerceStore
+ private val siteStore: SiteStore
) : ScopedViewModel(savedState) {
companion object {
const val JETPACK_SLUG = "jetpack"
@@ -114,10 +114,10 @@ class JetpackCPInstallViewModel @Inject constructor(
private suspend fun isJetpackConnectedAfterInstallation(): Boolean {
var attempt = 0
while (attempt < ATTEMPT_LIMIT) {
- val result = wooCommerceStore.fetchWooCommerceSites()
- val sites = result.model
- if (sites != null) {
- val syncedSite = sites.firstOrNull { it.siteId == selectedSite.get().siteId }
+ val siteId = selectedSite.get().siteId
+ val resultIsNotError = siteStore.fetchSite(selectedSite.get()).let { !it.isError }
+ if (resultIsNotError) {
+ val syncedSite = siteStore.getSiteBySiteId(siteId)
if (syncedSite?.isJetpackConnected == true && syncedSite.hasWooCommerce) {
selectedSite.set(syncedSite)
return true
@@ -125,6 +125,8 @@ class JetpackCPInstallViewModel @Inject constructor(
attempt++
delay(SYNC_CHECK_DELAY)
}
+ } else {
+ attempt++
}
}
return false
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jitm/JitmStoreInMemoryCache.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jitm/JitmStoreInMemoryCache.kt
index 8947d42baee..3feda096a66 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jitm/JitmStoreInMemoryCache.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jitm/JitmStoreInMemoryCache.kt
@@ -2,20 +2,20 @@ package com.woocommerce.android.ui.jitm
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.util.WooLog
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooResult
import org.wordpress.android.fluxc.network.rest.wpcom.wc.jitm.JITMApiResponse
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
import javax.inject.Singleton
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
@Singleton
class JitmStoreInMemoryCache
@@ -29,49 +29,42 @@ class JitmStoreInMemoryCache
) {
private val cache = ConcurrentHashMap>()
- @Volatile
- private var cacheInitContinuation: Continuation? = null
-
- @Volatile
- private var initialisationStatus = InitStatus.NOT_STARTED
+ private val initStatus = CompletableDeferred()
+ private val initMutex = Mutex()
suspend fun init() {
- if (!selectedSite.exists() || initialisationStatus != InitStatus.NOT_STARTED) return
-
- initialisationStatus = InitStatus.STARTED
+ if (!selectedSite.exists()) return
+
+ initMutex.withLock {
+ if (initStatus.isCompleted) return
+
+ supervisorScope {
+ pathsProvider.paths.map { path ->
+ async {
+ WooLog.d(WooLog.T.JITM, "Fetching JITM message for path: $path")
+ val response = jitmStore.fetchJitmMessage(
+ selectedSite.get(),
+ path,
+ jitmQueryParamsEncoder.getEncodedQueryParams(),
+ )
+ handleResponse(path, response)
+ }
+ }.awaitAll()
+ }
- supervisorScope {
- pathsProvider.paths.map { path ->
- async {
- WooLog.d(WooLog.T.JITM, "Fetching JITM message for path: $path")
- val response = jitmStore.fetchJitmMessage(
- selectedSite.get(),
- path,
- jitmQueryParamsEncoder.getEncodedQueryParams(),
- )
- handleResponse(path, response)
- }
- }.awaitAll()
+ initStatus.complete(Unit)
}
-
- cacheInitContinuation?.resume(Unit)
- initialisationStatus = InitStatus.DONE
}
suspend fun getMessagesForPath(messagePath: String): List {
WooLog.d(WooLog.T.JITM, "Getting JITM messages for path: $messagePath")
if (!selectedSite.exists()) return emptyList()
- when (initialisationStatus) {
- InitStatus.NOT_STARTED -> {
- appCoroutineScope.launch { init() }
- suspendCoroutine { cacheInitContinuation = it }
- }
- InitStatus.STARTED -> suspendCoroutine { cacheInitContinuation = it }
- InitStatus.DONE -> {
- // cache initialization is done, use it
- }
+ if (!initStatus.isCompleted) {
+ appCoroutineScope.launch { init() }
+ initStatus.await()
}
+
cache.putIfAbsent(messagePath, CopyOnWriteArrayList())
return cache[messagePath]!!
}
@@ -104,8 +97,4 @@ class JitmStoreInMemoryCache
private fun evictFirstMessage(messagePath: String) {
cache[messagePath]?.let { if (it.isNotEmpty()) it.removeAt(0) }
}
-
- private enum class InitStatus {
- NOT_STARTED, STARTED, DONE,
- }
}
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/accountmismatch/AccountMismatchErrorScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/accountmismatch/AccountMismatchErrorScreen.kt
index 696c73379ad..49d8baf2646 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/accountmismatch/AccountMismatchErrorScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/accountmismatch/AccountMismatchErrorScreen.kt
@@ -58,9 +58,9 @@ import com.woocommerce.android.ui.compose.component.WCOutlinedButton
import com.woocommerce.android.ui.compose.component.WCOutlinedTextField
import com.woocommerce.android.ui.compose.component.WCPasswordField
import com.woocommerce.android.ui.compose.component.WCTextButton
-import com.woocommerce.android.ui.compose.component.WCWebView
-import com.woocommerce.android.ui.compose.component.WebViewNavigator
-import com.woocommerce.android.ui.compose.component.rememberWebViewNavigator
+import com.woocommerce.android.ui.compose.component.web.WCWebView
+import com.woocommerce.android.ui.compose.component.web.WebViewNavigator
+import com.woocommerce.android.ui.compose.component.web.rememberWebViewNavigator
import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground
import com.woocommerce.android.ui.login.accountmismatch.AccountMismatchErrorViewModel.ViewState
import com.woocommerce.android.util.ChromeCustomTabUtils
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/connection/JetpackActivationWebViewScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/connection/JetpackActivationWebViewScreen.kt
index a73d369f075..7c51dd33570 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/connection/JetpackActivationWebViewScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/connection/JetpackActivationWebViewScreen.kt
@@ -10,7 +10,7 @@ import androidx.compose.ui.res.stringResource
import com.woocommerce.android.R
import com.woocommerce.android.ui.common.wpcomwebview.WPComWebViewAuthenticator
import com.woocommerce.android.ui.compose.component.Toolbar
-import com.woocommerce.android.ui.compose.component.WCWebView
+import com.woocommerce.android.ui.compose.component.web.WCWebView
import org.wordpress.android.fluxc.network.UserAgent
@Composable
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/sitecredentials/LoginSiteCredentialsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/sitecredentials/LoginSiteCredentialsScreen.kt
index 4e3edf67884..7040debd012 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/sitecredentials/LoginSiteCredentialsScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/sitecredentials/LoginSiteCredentialsScreen.kt
@@ -35,8 +35,8 @@ import com.woocommerce.android.ui.compose.component.WCColoredButton
import com.woocommerce.android.ui.compose.component.WCOutlinedTextField
import com.woocommerce.android.ui.compose.component.WCPasswordField
import com.woocommerce.android.ui.compose.component.WCTextButton
-import com.woocommerce.android.ui.compose.component.WCWebView
import com.woocommerce.android.ui.compose.component.getText
+import com.woocommerce.android.ui.compose.component.web.WCWebView
import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground
@Composable
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/sitecredentials/applicationpassword/ApplicationPasswordTutorialScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/sitecredentials/applicationpassword/ApplicationPasswordTutorialScreen.kt
index 9847ac74750..d072961f9ea 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/sitecredentials/applicationpassword/ApplicationPasswordTutorialScreen.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/sitecredentials/applicationpassword/ApplicationPasswordTutorialScreen.kt
@@ -32,7 +32,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import com.woocommerce.android.R
import com.woocommerce.android.ui.compose.component.Toolbar
-import com.woocommerce.android.ui.compose.component.WCWebView
+import com.woocommerce.android.ui.compose.component.web.WCWebView
import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground
import org.wordpress.android.fluxc.network.UserAgent
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivity.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivity.kt
index f20b5b142d3..f5efbf9010b 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivity.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivity.kt
@@ -81,6 +81,8 @@ import com.woocommerce.android.ui.main.MainActivityViewModel.RestartActivityForP
import com.woocommerce.android.ui.main.MainActivityViewModel.ShortcutOpenOrderCreation
import com.woocommerce.android.ui.main.MainActivityViewModel.ShortcutOpenPayments
import com.woocommerce.android.ui.main.MainActivityViewModel.ShowFeatureAnnouncement
+import com.woocommerce.android.ui.main.MainActivityViewModel.ViewBlazeCampaignDetail
+import com.woocommerce.android.ui.main.MainActivityViewModel.ViewBlazeCampaignList
import com.woocommerce.android.ui.main.MainActivityViewModel.ViewMyStoreStats
import com.woocommerce.android.ui.main.MainActivityViewModel.ViewOrderDetail
import com.woocommerce.android.ui.main.MainActivityViewModel.ViewOrderList
@@ -113,6 +115,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.wordpress.android.login.LoginAnalyticsListener
import org.wordpress.android.login.LoginMode
import org.wordpress.android.util.NetworkUtils
+import java.lang.ref.WeakReference
import java.math.BigDecimal
import java.util.Locale
import javax.inject.Inject
@@ -207,8 +210,30 @@ class MainActivity :
private var progressDialog: ProgressDialog? = null
private val fragmentLifecycleObserver: FragmentLifecycleCallbacks = object : FragmentLifecycleCallbacks() {
+ private var lastFragment = WeakReference(null)
+
override fun onFragmentViewCreated(fm: FragmentManager, f: Fragment, v: View, savedInstanceState: Bundle?) {
+ updateAppBarAndBottomNav(f)
+ }
+
+ override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {
+ // This logic is needed to handle this case:
+ // 1. User navigates from Fragment A to Fragment B
+ // 2. Fragment B's view gets created, and onFragmentViewCreated is called, updating the AppBar.
+ // 3. Quickly the user goes back to Fragment A
+ // 4. Fragment A's view wasn't destroyed yet, so it doesn't go through the creation lifecycle,
+ // which means onFragmentViewCreated won't be called, and the AppBar won't be updated.
+ //
+ // In this case, lastFragment will be pointing to Fragment B, so we can compare it with the fragment being
+ // started (Fragment A), and we can update the AppBar accordingly.
+ if (lastFragment.get() != f) {
+ updateAppBarAndBottomNav(f)
+ }
+ }
+
+ private fun updateAppBarAndBottomNav(f: Fragment) {
if (f is DialogFragment) return
+ lastFragment = WeakReference(f)
when (val appBarStatus = (f as? BaseFragment)?.activityAppBarStatus ?: AppBarStatus.Visible()) {
is AppBarStatus.Visible -> {
@@ -763,7 +788,7 @@ class MainActivity :
intent.removeExtra(FIELD_REMOTE_NOTIFICATION)
intent.removeExtra(FIELD_PUSH_ID)
- viewModel.handleIncomingNotification(localPushId, notification)
+ viewModel.onPushNotificationTapped(localPushId, notification)
} else if (localNotification != null) {
intent.removeExtra(FIELD_LOCAL_NOTIFICATION)
viewModel.onLocalNotificationTapped(localNotification)
@@ -780,6 +805,8 @@ class MainActivity :
is ViewOrderDetail -> showOrderDetail(event)
is ViewReviewDetail -> showReviewDetail(event.uniqueId, launchedFromNotification = true)
is ViewReviewList -> showReviewList()
+ is ViewBlazeCampaignDetail -> showBlazeCampaignList(event.campaignId, event.isOpenedFromPush)
+ ViewBlazeCampaignList -> showBlazeCampaignList(campaignId = null)
is RestartActivityEvent -> onRestartActivityEvent(event)
is ShowFeatureAnnouncement -> navigateToFeatureAnnouncement(event)
is ViewUrlInWebView -> navigateToWebView(event)
@@ -820,6 +847,18 @@ class MainActivity :
observeBottomBarState()
}
+ private fun showBlazeCampaignList(campaignId: String?, isOpenedFromPush: Boolean = false) {
+ binding.bottomNav.currentPosition = MORE
+ binding.bottomNav.active(MORE.position)
+
+ navController.navigateSafely(
+ MoreMenuFragmentDirections.actionMoreMenuToBlazeCampaignListFragment(
+ campaignId = campaignId
+ ),
+ skipThrottling = isOpenedFromPush
+ )
+ }
+
private fun observeNotificationsPermissionBarVisibility() {
viewModel.isNotificationsPermissionCardVisible.observe(this) { isVisible ->
if (isVisible) {
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivityViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivityViewModel.kt
index b9c288824db..ee677b9f82c 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivityViewModel.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/main/MainActivityViewModel.kt
@@ -15,7 +15,9 @@ import com.woocommerce.android.model.FeatureAnnouncement
import com.woocommerce.android.model.Notification
import com.woocommerce.android.notifications.NotificationChannelType
import com.woocommerce.android.notifications.UnseenReviewsCountHandler
+import com.woocommerce.android.notifications.WooNotificationType
import com.woocommerce.android.notifications.local.LocalNotificationType
+import com.woocommerce.android.notifications.local.LocalNotificationType.BLAZE_ABANDONED_CAMPAIGN_REMINDER
import com.woocommerce.android.notifications.local.LocalNotificationType.BLAZE_NO_CAMPAIGN_REMINDER
import com.woocommerce.android.notifications.push.NotificationMessageHandler
import com.woocommerce.android.tools.SelectedSite
@@ -109,7 +111,7 @@ class MainActivityViewModel @Inject constructor(
)
}
- fun handleIncomingNotification(localPushId: Int, notification: Notification?) {
+ fun onPushNotificationTapped(localPushId: Int, notification: Notification?) {
notification?.let {
// update current selectSite based on the current notification
val currentSite = selectedSite.get()
@@ -118,8 +120,8 @@ class MainActivityViewModel @Inject constructor(
changeSiteAndRestart(it.remoteSiteId, RestartActivityForPushNotification(localPushId, notification))
} else {
when (localPushId) {
- it.getGroupPushId() -> onGroupMessageOpened(it.channelType, it.remoteSiteId)
- else -> onSingleNotificationOpened(localPushId, it)
+ it.getGroupPushId() -> onGroupMessageOpened(it)
+ else -> onSinglePushNotificationOpened(localPushId, it)
}
}
} ?: run {
@@ -170,29 +172,50 @@ class MainActivityViewModel @Inject constructor(
}
}
- private fun onGroupMessageOpened(notificationChannelType: NotificationChannelType, remoteSiteId: Long) {
- notificationHandler.markNotificationsOfTypeTapped(notificationChannelType)
- notificationHandler.removeNotificationsOfTypeFromSystemsBar(notificationChannelType, remoteSiteId)
- when (notificationChannelType) {
+ private fun onGroupMessageOpened(notification: Notification) {
+ notificationHandler.markNotificationsOfTypeTapped(notification.channelType)
+ notificationHandler.removeNotificationsOfTypeFromSystemsBar(notification.channelType, notification.remoteSiteId)
+ when (notification.channelType) {
NotificationChannelType.NEW_ORDER -> triggerEvent(ViewOrderList)
NotificationChannelType.REVIEW -> triggerEvent(ViewReviewList)
- else -> triggerEvent(ViewMyStoreStats)
+ NotificationChannelType.OTHER -> if (notification.isBlazeNotification) {
+ triggerEvent(ViewBlazeCampaignList)
+ } else {
+ triggerEvent(ViewMyStoreStats)
+ }
}
}
- private fun onSingleNotificationOpened(localPushId: Int, notification: Notification) {
+ private fun onSinglePushNotificationOpened(localPushId: Int, notification: Notification) {
notificationHandler.markNotificationTapped(notification.remoteNoteId)
notificationHandler.removeNotificationByNotificationIdFromSystemsBar(localPushId)
- if (notification.channelType == NotificationChannelType.REVIEW) {
- analyticsTrackerWrapper.track(REVIEW_OPEN)
- triggerEvent(ViewReviewDetail(notification.uniqueId))
- } else if (notification.channelType == NotificationChannelType.NEW_ORDER) {
- if (siteStore.getSiteBySiteId(notification.remoteSiteId) != null) {
- triggerEvent(ViewOrderDetail(notification.uniqueId, notification.remoteNoteId))
- } else {
- // the site does not exist locally, open order list
- triggerEvent(ViewOrderList)
+ when (notification.noteType) {
+ is WooNotificationType.NewOrder -> {
+ when {
+ siteStore.getSiteBySiteId(notification.remoteSiteId) != null -> triggerEvent(
+ ViewOrderDetail(
+ notification.uniqueId,
+ notification.remoteNoteId
+ )
+ )
+
+ else -> triggerEvent(ViewOrderList)
+ }
}
+
+ is WooNotificationType.ProductReview -> {
+ analyticsTrackerWrapper.track(REVIEW_OPEN)
+ triggerEvent(ViewReviewDetail(notification.uniqueId))
+ }
+
+ is WooNotificationType.BlazeStatusUpdate -> triggerEvent(
+ ViewBlazeCampaignDetail(
+ campaignId = notification.uniqueId.toString(),
+ isOpenedFromPush = true
+ )
+ )
+
+ is WooNotificationType.LocalReminder -> error("Local reminder notification should not be handled here")
}
}
@@ -264,7 +287,8 @@ class MainActivityViewModel @Inject constructor(
)
LocalNotificationType.fromString(notification.tag)?.let {
when (it) {
- BLAZE_NO_CAMPAIGN_REMINDER -> triggerEvent(LaunchBlazeCampaignCreation)
+ BLAZE_NO_CAMPAIGN_REMINDER,
+ BLAZE_ABANDONED_CAMPAIGN_REMINDER -> triggerEvent(LaunchBlazeCampaignCreation)
}
}
}
@@ -312,6 +336,7 @@ class MainActivityViewModel @Inject constructor(
data class ViewUrlInWebView(
val url: String,
) : Event()
+
object ShortcutOpenPayments : Event()
object ShortcutOpenOrderCreation : Event()
object LaunchBlazeCampaignCreation : Event()
@@ -328,6 +353,8 @@ class MainActivityViewModel @Inject constructor(
data class ShowFeatureAnnouncement(val announcement: FeatureAnnouncement) : Event()
data class ViewReviewDetail(val uniqueId: Long) : Event()
data class ViewOrderDetail(val uniqueId: Long, val remoteNoteId: Long) : Event()
+ data class ViewBlazeCampaignDetail(val campaignId: String, val isOpenedFromPush: Boolean) : Event()
+ object ViewBlazeCampaignList : Event()
data class ShowPrivacyPreferenceUpdatedFailed(val analyticsEnabled: Boolean) : Event()
object ShowPrivacySettings : Event()
data class ShowPrivacySettingsWithError(val requestedAnalyticsValue: RequestedAnalyticsValue) : Event()
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/moremenu/MoreMenuViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/moremenu/MoreMenuViewModel.kt
index c997117de2d..da9b6a78fa3 100644
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/moremenu/MoreMenuViewModel.kt
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/moremenu/MoreMenuViewModel.kt
@@ -34,7 +34,6 @@ import com.woocommerce.android.ui.payments.taptopay.isAvailable
import com.woocommerce.android.ui.plans.domain.SitePlan
import com.woocommerce.android.ui.plans.repository.SitePlanRepository
import com.woocommerce.android.ui.woopos.WooPosIsEnabled
-import com.woocommerce.android.ui.woopos.WooPosIsFeatureFlagEnabled
import com.woocommerce.android.util.WooLog
import com.woocommerce.android.viewmodel.ResourceProvider
import com.woocommerce.android.viewmodel.ScopedViewModel
@@ -71,7 +70,6 @@ class MoreMenuViewModel @Inject constructor(
private val isGoogleForWooEnabled: IsGoogleForWooEnabled,
private val hasGoogleAdsCampaigns: HasGoogleAdsCampaigns,
private val isWooPosEnabled: WooPosIsEnabled,
- private val isWooPosFFEnabled: WooPosIsFeatureFlagEnabled,
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper
) : ScopedViewModel(savedState) {
private var storeHasGoogleAdsCampaigns = false
@@ -459,24 +457,17 @@ class MoreMenuViewModel @Inject constructor(
.onStart { emit("") }
private fun checkFeaturesAvailability(): Flow
+
+
+
diff --git a/WooCommerce/src/main/res/navigation/nav_graph_custom_fields.xml b/WooCommerce/src/main/res/navigation/nav_graph_custom_fields.xml
new file mode 100644
index 00000000000..ee777fdb50e
--- /dev/null
+++ b/WooCommerce/src/main/res/navigation/nav_graph_custom_fields.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WooCommerce/src/main/res/navigation/nav_graph_main.xml b/WooCommerce/src/main/res/navigation/nav_graph_main.xml
index 5a64cb374b9..e7777a93224 100644
--- a/WooCommerce/src/main/res/navigation/nav_graph_main.xml
+++ b/WooCommerce/src/main/res/navigation/nav_graph_main.xml
@@ -811,6 +811,11 @@
android:name="isPostCampaignCreation"
android:defaultValue="false"
app:argType="boolean" />
+
+
+
+
+
+
+
+
-
+ مفتاح غير صالح: يرجى إزالة رمز \"_\" من البداية.
+ هذا المفتاح مستخدم بالفعل لحقل مخصص آخر.\nلا يدعم التطبيق حاليًا إنشاء مفاتيح مكررة. يرجى استخدام مسؤول ووردبريس لتكرار مفتاح إذا لزم الأمر.
+ إضافة حقول مخصصة
+ تم حذف الحقل المخصص
+ فشل حفظ التغييرات، يرجى المحاولة مجددًا
+ تم حفظ التغييرات
+ حفظ التغييرات
+ يبدو أنك غير متصل بالإنترنت. تأكد من تشغيل شبكة Wi-Fi الخاصة بك. إذا كنت تستخدم بيانات الهاتف المحمول، فتأكد من تمكينها ضمن إعدادات الجهاز لديك.
+ فشل الفحص. يرجى المحاولة مجددًا في وقت لاحق
+ القيمة
+ المفتاح
+ أما أنواع المنتجات الأخرى، مثل: المنتجات المتغيرة والظاهرية، ستتوفر في التحديثات المستقبلية.
+ لا يمكن استخدام إلا المنتجات المادية البسيطة التي تتضمن نقطة بيع الآن.
+ إلغاء
+ المدة
+ سيتم تشغيل الحملة حتى تُوقفها.
+ تحديد المدة
+ إلى %1$s
+ الجدولة
+ الإنفاق اليومي
+ ما المبلغ الذي ترغب في إنفاقه على حملتك، وما مقدار الوقت الذي ينبغي أن تستمر فيه؟
+ %1$s ← %2$s
+ جعل منتجاتك مرئية لملايين الأشخاص من خلال Blaze وتعزيز مبيعاتك
+ هل تفكر في تعزيز مبيعاتك؟
+ خطأ في أثناء تحميل الحقول المخصصة
+ حقول مخصصة
+ خلفية معتمة. انقر لتجاهل مربع الحوار.
+ %1$s أسبوعيًا
+ التشغيل حتى أوقفه
+ مستمر بدءًا من %1$s
+ الإنفاق الأسبوعي
+ %1$s أسبوعيًا، بدءًا من %2$s
+ أسبوعيًا
+ المتبقي
+ الإجمالي
+ مرات النقر
+ يبدو أن الجهاز في وضع توفير الطاقة. \nيتعذر علينا تقديم معلومات المتجر الخاصة بك بينما يكون نشطًا.
+ القائمة المنبثقة مع الخيارات. مرر للتنقل عبر العناصر.
+ فتح قائمة شريط الأدوات
+ شريط أدوات يتضمن حالة قارئ البطاقات. القائمة مفتوحة. انقر نقرًا مزدوجًا للتفاعل.
+ شريط أدوات يتضمن حالة قارئ البطاقات. انقر نقرًا مزدوجًا للتفاعل.
+ تم تعطيل القائمة
+ تم تمكين القائمة
+ قارئ البطاقات غير متصل. النقر نقرًا مزدوج للاتصال
+ قارئ البطاقات غير متصل
+ خلفية خافتة. انقر لإغلاق القائمة.
+ أيقونة علامة الاختيار للدفع الناجح
+ إزالة هذا العنصر من عربة التسوق
+ سيتم فقدان أي طلبات قيد التقدم.
+ هل تريد إنهاء وضع نقطة البيع؟
+ إغلاق
+ خلفية خافتة. انقر لتجاهل مربع الحوار.
+ انقر نقرًا مزدوجًا لتجاهل مربع الحوار
+ مربع حوار المنتجات البسيطة فقط
+ النقر نقرًا مزدوجًا للتعرف على المزيد
+ شعار المنتجات البسيطة فقط
+ روِّج لمنتجاتك من خلال Blaze Ads وارفع مبيعاتك الآن.
+ تعزيز مبيعاتك
+ تلقي المدفوعات في أثناء التنقل
+ تم إدخال رمز PIN غير صحيح. المحاولة الآن، أو استخدام وسائل دفع أخرى
القائمة
المنتج في عربة التسوق %s، السعر %s
المنتج %s، السعر %s
@@ -14,8 +74,7 @@ Language: ar
طلب جديد
موافق
+ إنشاء طلب في إدارة المتجر
- لتلقي دفعة مقابل منتج غير بسيط، اخرج من نقطة البيع وأنشئ طلبًا جديدًا من علامة تبويب الطلبات.
- لا يمكن استخدام إلا المنتجات المادية البسيطة التي تتضمن نقطة بيع الآن.\nأما أنواع المنتجات الأخرى، مثل: المنتجات المتغيرة والظاهرية، ستتوفر في التحديثات المستقبلية.
+ لتلقي دفعة مقابل منتج غير بسيط، اخرج من نقطة البيع وأنشئ طلبًا جديدًا من علامة تبويب الطلبات.
لماذا يتعذر علي رؤية منتجاتي؟
معلومات
إغلاق
@@ -39,8 +98,6 @@ Language: ar
لا يدعم POS حاليًا سوى المنتجات البسيطة - \nأنشئ واحدًا هنا للبدء.
لم يتم العثور على منتجات مدعومة
لا توجد منتجات
- لنقدم خدمات إلى بعض العملاء
- تشغيل
الحصول على الدعم
توصيل القارئ الخاص بك
تمت إزالة الصورة
@@ -135,8 +192,7 @@ Language: ar
لا توجد قواعد للكميات
الجمهور
إلغاء
- إنهاء
- هل تريد بالتأكيد إنهاء POS؟
+ إنهاء
إنهاء POS
إزالة %s من عربة التسوق
السداد
@@ -419,20 +475,16 @@ Language: ar
الشعار
تغيير الصورة
تطبيق
- يبدأ في
+ تاريخ البدء
%1$s من الأيام
- تعيين المدة
تعكس الانطباعات عدد مرات ظهور إعلانك للعملاء المحتملين.\n\n\n على الرغم من تعذر التأكد من الأرقام الدقيقة بسبب تأرجح حركة المرور على الإنترنت وسلوك المستخدمين، فإننا نهدف إلى مطابقة الانطباعات الفعلية على إعلانك قدر المستطاع مع العدد المستهدف.\n\n\n تذكر أن الانطباعات تدور حول الرؤية وليس الإجراء الذي يتخذه المشاهدون.
تم
الانطباعات
تحديث
تحرير
- المدة
عدد الأشخاص المتوقع وصولهم يوميًا
%1$s يوميًا
لمدة %1$s من الأيام
- إجمالي الإنفاق
- ما المبلغ الذي تود إنفاقه على حملة الترويج لمنتجك؟
تعيين ميزانيتك
الكل
%1$s من الأيام من %2$s
@@ -634,7 +686,6 @@ Language: ar
جذب مزيد من المبيعات إلى متجرك باستخدام Blaze
كان هناك خطأ في أثناء تحديث قائمة الحملات. ترجى المحاولة مرة أخرى لاحقًا.
تحديد مصدر الوسائط
- حدث خطأ في أثناء إنشاء اسم المنتج ووصفه.
لم يتم الكشف عن نص. يرجى تحديد صورة حزم أخرى أو إدخال تفاصيل المنتج يدويًا.
إضافة منتج
مسح الرمز الشريطي ضوئيًا
@@ -656,9 +707,6 @@ Language: ar
جرّب مدفوعات %s من خلال بطاقة الخصم أو الائتمان لديك.\nستُستَرد المدفوعات عندما تنتهي.
إنها سهلة وآمنة وخاصة.
اقبل كل أنواع المدفوعات الشخصية، مباشرة\nعلى هاتفك. لا يلزم أجهزة إضافة.
- الميزانية
- النقرات
- الانطباعات
مرفوض
مكتمل
نشط
@@ -691,46 +739,23 @@ Language: ar
الإعدادات
إضافة التفاصيل يدويًا باستخدام البريد الإلكتروني
تتعذر علينا معالجة طلب تسجيل الدخول إلى تطبيقك
- خطأ في أثناء نسخ اسم المنتج إلى الحافظة.
- تم نسخ اسم المنتج إلى الحافظة.
- تم إنشاء اسم المنتج بواسطة الذكاء الاصطناعي
خطأ في أثناء نسخ وصف المنتج إلى الحافظة.
تم نسخ وصف المنتج إلى الحافظة.
فشل إنشاء المنتج. يرجى المحاولة مجددًا
مسح العنوان وإيقاف استخدام هذا المعدل
تعيين معدل ضريبة جديد لهذا الطلب
إضافة معدل الضريبة تلقائيًا
- كانت هناك مشكلة في أثناء إنشاء اسم المنتج. ترجى المحاولة مجددًا.
- إعادة الإنشاء
- كتابة ذلك من أجلي
- أخبرنا بمضمون منتجك وما الذي يجعله فريدًا!
- السماح للذكاء الاصطناعي بإنشاء عناوين جذابة من أجلك
- اسم المنتج
حاول تعطيل عامل التصفية غير المقروء لرؤية كل مراجعات منتجك
لا توجد مراجعات غير مقروءة للمنتج
- تم تحديد النغمة
الإقناع
زهري
رسمي
عادي
- قم بتعيين النغمة والصوت لتشكيل العرض التقديمي لمنتجك الذي يتوافق مع علامتك التجارية.
النغمة والصوت
تفاصيل
- وصف المنتج
اسم المنتج
- يمكنك دومًا تغيير التفاصيل أدناه لاحقًا.
معاينة
- إنشاء تفاصيل المنتجات
- تعيين النغمة والصوت
- أضف الميزات الأساسية أو المزايا أو التفاصيل لمساعدة منتجك على الظهور على الإنترنت.
على سبيل المثال، قماش ناعم وخياطة متينة وتصميم فريد
- أبرز ما يجعل منتجك فريدًا، واسمح للذكاء الاصطناعي بنثر سحره.
- نبذة عن منتجك
- اقتراح اسم
- على سبيل المثال: قماش ناعم وخياطة متينة وتصميم فريد.
- اسم المنتج
- أو وسِّع نطاق اختياراتك عن طريق النقر للحصول على مزيد من اقتراحات الأسماء.
- إضافة اسم منتجك
مدعوم من الذكاء الاصطناعي. <a href=\'guidelines\'><u>تعرّف على المزيد</u></a>.
إضافة منتج والتفاصيل يدويًا
إضافة يدويًا
@@ -1054,7 +1079,6 @@ Language: ar
زيادة المبيعات من خلال العروض الخاصة
عرض متجرك
البقاء على اطلاع
- الانضمام إلى المدفوعات عبر الهاتف المحمول
إدارة المزيد على المسؤول
عام
الإعدادات
diff --git a/WooCommerce/src/main/res/values-de/strings.xml b/WooCommerce/src/main/res/values-de/strings.xml
index d6deae43422..b073e720ba4 100644
--- a/WooCommerce/src/main/res/values-de/strings.xml
+++ b/WooCommerce/src/main/res/values-de/strings.xml
@@ -1,11 +1,71 @@
+ Ungültiger Schlüssel: Bitte entferne „_“ vom Anfang des Schlüssels.
+ Dieser Schlüssel wird bereits für ein anderes individuelles Feld verwendet.\nDie App unterstützt derzeit nicht das Erstellen von doppelten Schlüsseln. Bitte verwende wp-admin, um einen Schlüssel zu duplizieren, falls dies erforderlich ist.
+ Individuelle Felder hinzufügen
+ Individuelles Feld gelöscht
+ Die Änderungen konnten nicht gespeichert werden, bitte versuche es erneut
+ Änderungen gespeichert
+ Änderungen werden gespeichert
+ Du bist anscheinend nicht mit dem Internet verbunden. Stelle sicher, dass dein WLAN eingeschaltet ist. Falls du mobile Daten verwendest, solltest du prüfen, ob sie in deinen Geräteeinstellungen aktiviert sind.
+ Scannen fehlgeschlagen. Bitte versuche es später erneut
+ Wert
+ Schlüssel
+ Weitere Produkttypen, wie variable und virtuelle, werden in künftigen Updates bereitgestellt.
+ Derzeit können nur einfache physische Produkte mit POS verwendet werden.
+ Abbrechen
+ Dauer
+ Die Kampagne läuft, bis du sie beendest.
+ Dauer angeben
+ bis zum %1$s
+ Zeitplan
+ Ausgaben für einen Tag
+ Wie viel Geld willst du für deine Kampagne ausgeben und wie lange soll sie laufen?
+ %1$s ➔ %2$s
+ Präsentiere deine Produkte mit Blaze Millionen von Interessenten und erhöhe deine Verkaufszahlen
+ Möchtest du deine Verkaufszahlen erhöhen?
+ Fehler beim Laden der individuellen Felder
+ Individuelle Felder
+ Abgedunkelter Hintergrund. Zum Schließen des Dialogs tippen.
+ %1$s wöchentlich
+ Ausführen, bis ich sie beende
+ Seit dem %1$s
+ Ausgaben für eine Woche
+ %1$s wöchentlich, ab dem %2$s
+ Wöchentlich
+ Verbleibend
+ Gesamt
+ Klickraten
+ Für dein Gerät wurde der Energiesparmodus aktiviert. \nSolange er aktiviert ist, können wir keine Informationen zu deinem Shop bereitstellen
+ Pop-up-Menü mit Optionen. Wische, um zwischen den Artikeln zu navigieren.
+ Werkzeugleiste öffnen
+ Werkzeugleiste mit Status des Kartenlesegeräts. Das Menü ist geöffnet. Zum Interagieren zweimal tippen.
+ Werkzeugleiste mit Status des Kartenlesegeräts. Zum Interagieren zweimal tippen.
+ Menü deaktiviert
+ Menü aktiviert
+ Kartenlesegerät nicht verbunden. Zum Auswählen zweimal tippen
+ Kartenlesegeräte verbunden
+ Abgedunkelter Hintergrund. Zum Schließen des Menüs tippen.
+ Häkchen-Icon bei erfolgreicher Zahlung
+ Diesen Artikel aus dem Warenkorb entfernen
+ Alle Bestellungen, die sich gerade in Bearbeitung befinden, gehen verloren.
+ Modus „Verkaufsort“ (POS) beenden?
+ Schließen
+ Abgedunkelter Hintergrund. Zum Schließen des Dialogs tippen.
+ Zum Schließen des Dialogs zweimal tippen
+ Dialog für einfache Produkte
+ Zweimal tippen, um weitere Informationen zu erhalten
+ Banner für einfache Produkte
+ Bewerbe deine Produkte mit Blaze-Werbung und steigere jetzt deinen Umsatz.
+ Erhöhe deinen Umsatz
+ Erhalte Zahlungen auch unterwegs
+ Es wurde eine falsche PIN eingegeben. Versuche es erneut oder verwende eine andere Zahlungsmethode
Menü
Produkt im Warenkorb %s, Preis %s
Produkt %s, Preis %s
@@ -14,12 +74,11 @@ Language: de
Neue Bestellung
OK
+ Eine Bestellung im Shop-Management erstellen
- Um die Zahlung für ein nicht einfaches Produkt entgegenzunehmen, beende POS und erstelle eine neue Bestellung über den Tab „Bestellungen“.
- Derzeit können nur einfache physische Produkte mit POS verwendet werden.\nWeitere Produkttypen, wie variable und virtuelle, werden in künftigen Updates bereitgestellt.
+ Um die Zahlung für ein nicht einfaches Produkt entgegenzunehmen, beende POS und erstelle eine neue Bestellung über den Tab „Bestellungen“.
Warum werden mir meine Produkte nicht angezeigt?
Info
Schließen
- Weitere Informationen
+ Mehr\u00A0erfahren
Derzeit sind nur einfache physische Produkte mit POS kompatibel. Weitere Produkttypen, wie variable und virtuelle, werden in künftigen Updates bereitgestellt.
Es werden nur einfache Produkte angezeigt
Website-Adresse
@@ -39,8 +98,6 @@ Language: de
POS unterstützt derzeit nur einfache Produkte – \nerstelle eins, um anzufangen.
Keine unterstützten Produkte gefunden
Keine Produkte
- Zeit, deine Kunden glücklich zu machen
- Wird geladen
Erhalte Support
Kartenlesegerät verbinden
Foto entfernt
@@ -135,8 +192,7 @@ Language: de
Keine Mengenregeln
Zielgruppe
Abbrechen
- Beenden
- Bist du sicher, dass du POS beenden möchtest?
+ Beenden
POS beenden
%s aus Warenkorb entfernen
Bezahlen
@@ -419,20 +475,16 @@ Language: de
Untertitel
Bild ändern
Übernehmen
- Beginnt
+ Startdatum
%1$s Tage
- Dauer festlegen
Aufrufe geben die Häufigkeit an, mit der deine Werbung potenziellen Kunden angezeigt wird.\n\n\n Auch wenn aufgrund von schwankendem Internet-Traffic und Nutzerverhalten keine genauen Zahlen garantiert werden können, versuchen wir, die tatsächlichen Aufrufe deiner Werbeanzeige so genau wie möglich mit deiner Zielzahl abzugleichen.\n\n\n Denke daran, dass es bei Aufrufen um die Sichtbarkeit geht und nicht um vom Betrachter ausgeführte Aktionen.
Fertig
Aufrufe
Update
Bearbeiten
- Dauer
Geschätzte Anzahl erreichter Personen pro Tag
%1$s täglich
für %1$s Tage
- Gesamtbestellwert
- Wie viel möchtest du für deine Produkt-Werbekampagne ausgeben?
Dein Budget festlegen
Alle
%1$s Tage seit %2$s
@@ -634,7 +686,6 @@ Language: de
Mit Blaze die Umsätze deines Shops steigern
Beim Aktualisieren der Kampagnenliste ist ein Fehler aufgetreten. Bitte versuche es später erneut.
Medienquelle auswählen
- Beim Generieren von Produktname und -beschreibung ist ein Fehler aufgetreten.
Kein Text erkannt. Bitte wähle ein anderes Foto der Verpackung aus oder gib die Produktdetails manuell ein.
Produkt hinzufügen
Barcode scannen
@@ -656,9 +707,6 @@ Language: de
Probiere eine %s-Zahlung mit deiner Debit- oder Kreditkarte aus.\nDie Zahlung wird zurückerstattet, wenn du fertig bist.
Es ist einfach, sicher und privat.
Du kannst alle Arten von persönlichen Zahlungen akzeptieren, direkt\nauf deinem Mobiltelefon. Es ist keine zusätzliche Hardware erforderlich.
- Budget
- Klicks
- Aufrufe
Abgelehnt
Abgeschlossen
Aktiv
@@ -691,46 +739,23 @@ Language: de
EINSTELLUNGEN
Details anhand der E-Mail-Adresse manuell hinzufügen
Deine App-Anmeldeanfrage konnte nicht verarbeitet werden
- Fehler beim Kopieren des Produktnamens in die Zwischenablage.
- Produktname in die Zwischenablage kopiert.
- Produktname von KI generiert
Fehler beim Kopieren der Produktbeschreibung in die Zwischenablage.
Produktbeschreibung in die Zwischenablage kopiert.
Produkterstellung ist fehlgeschlagen. Bitte versuche es erneut
Adresse löschen und diesen Satz nicht mehr verwenden
Neuen Steuersatz für diese Bestellung festlegen
Steuersatz wird automatisch hinzugefügt
- Beim Generieren des Produktnamens ist ein Problem aufgetreten. Bitte versuche es erneut.
- Neu generieren
- Für mich schreiben
- Erzähle uns mehr von deinem Produkt und was es einzigartig macht!
- Lasse die KI ansprechende Titel für dich generieren
- Produktname
Deaktiviere den Filter für ungelesene Produktbewertungen, damit dir alle angezeigt werden
Keine ungelesenen Produktbewertungen
- Tonalität ausgewählt
Überzeugend
Blumig
Formell
Locker
- Lege Sprache und Tonalität fest, um die Präsentation deines Produkts an deine Marke anzupassen.
Sprache und Tonalität
Details
- Produktbeschreibung
Produktname
- Du kannst die unten angegebenen Informationen später jederzeit ändern.
Vorschau
- Produktdetails erstellen
- Sprache und Tonalität festlegen
- Füge wichtige Funktionen, Vorteile oder Details hinzu, damit dein Produkt online besser gefunden wird.
Zum Beispiel: weicher Stoff, langlebige Absteppung, einzigartiges Design
- Hebe hervor, was dein Produkt einzigartig macht – die KI übernimmt den Rest.
- Über dein Produkt
- Namen vorschlagen
- Zum Beispiel: weicher Stoff, langlebige Absteppung, einzigartiges Design.
- Produktname
- Oder erweitere deine Auswahl, indem du tippst, um weitere Namensvorschläge zu erhalten.
- Deinen Produktnamen hinzufügen
Mit Unterstützung von KI. <a href=\'guidelines\'><u>Weitere Informationen</u></a>.
Füge ein Produkt und die entsprechenden Details manuell hinzu
Manuell hinzufügen
@@ -1054,7 +1079,6 @@ Language: de
Steigere deine Umsätze mit speziellen Angeboten
Deinen Shop ansehen
Bleibe auf dem Laufenden
- Nutze mobile Zahlungen
Verwalte mehr als Admin
Allgemeines
Einstellungen
diff --git a/WooCommerce/src/main/res/values-es/strings.xml b/WooCommerce/src/main/res/values-es/strings.xml
index a0cb0b40d02..8a94b3295a3 100644
--- a/WooCommerce/src/main/res/values-es/strings.xml
+++ b/WooCommerce/src/main/res/values-es/strings.xml
@@ -1,11 +1,71 @@
+ Clave no válida: elimina el carácter «_» del principio.
+ Esta clave ya se utiliza para otro campo personalizado.\nActualmente, la aplicación no permite crear claves duplicadas. Utiliza wp-admin para duplicar una clave si es necesario.
+ Añadir campos personalizados
+ Campo personalizado borrado
+ No se han podido guardar los cambios; inténtalo de nuevo
+ Cambios guardados
+ Guardando cambios
+ Parece que no estás conectado a Internet. Comprueba que tienes activado el wifi. Si te vas a conectar con datos móviles, asegúrate de que estén activados en los ajustes de tu dispositivo.
+ No se ha podido realizar la exploración. Inténtalo de nuevo más tarde
+ Valor
+ Clave
+ Otros tipos de productos, como los variables y los virtuales, estarán disponibles en futuras actualizaciones.
+ En estos momentos, solo se pueden utilizar productos físicos sencillos con TPV.
+ Cancelar
+ Duración
+ La campaña continuará hasta que la detengas.
+ Especificar la duración
+ a %1$s
+ Programar
+ Gasto diario
+ ¿Cuánto te gustaría gastar en tu campaña y cuánto tiempo debería durar?
+ %1$s ➔ %2$s
+ Consigue que tus productos sean vistos por millones de personas con Blaze e impulsa tus ventas
+ ¿Quieres aumentar tus ventas?
+ Error al cargar los campos personalizados
+ Campos personalizados
+ Fondo atenuado. Toca para cerrar el diálogo.
+ %1$s a la semana
+ Ejecutar hasta que yo lo detenga
+ En curso a partir del %1$s
+ gasto semanal
+ %1$s a la semana a partir del %2$s
+ A la semana
+ Restante
+ Total
+ Clics
+ Parece que tu dispositivo está en modo de ahorro de batería. \nNo podemos facilitarte información sobre tu tienda mientras esté activo
+ Menú emergente con opciones. Desliza el dedo para navegar por los elementos.
+ Abrir el menú de la barra de herramientas
+ Barra de herramientas con el estado del lector de tarjetas. El menú está abierto. Toca dos veces para interactuar.
+ Barra de herramientas con el estado del lector de tarjetas. Toca dos veces para interactuar.
+ Menú deshabilitado
+ Menú habilitado
+ Lector de tarjetas no conectado. Toca dos veces para conectarte
+ Lector de tarjetas conectado
+ Fondo atenuado. Toca para cerrar el menú.
+ Icono de pago efectuado con éxito
+ Eliminar este elemento del carrito
+ Se perderán los pedidos en curso.
+ ¿Salir del modo de punto de venta?
+ Cerrar
+ Fondo atenuado. Toca para cerrar el diálogo.
+ Toca dos veces para cerrar el diálogo
+ Diálogo de solo productos de tipo simple
+ Toca dos veces para obtener más información
+ Banner de solo productos de tipo simple
+ Promociona tus productos con Blaze Ads y aumenta tus ventas ahora.
+ Aumenta tus ventas
+ Acepta pagos sobre la marcha
+ Se ha introducido un PIN incorrecto. Inténtalo de nuevo o utiliza otro método de pago
Menú
Producto en el carrito %s, precio %s
Producto %s, precio %s
@@ -14,12 +74,11 @@ Language: es
Nuevo pedido
Aceptar
+ Crear un pedido en la gestión de la tienda
- Para aceptar el pago de un producto no sencillo, sal de TPV y crea un nuevo pedido desde la pestaña de pedidos.
- En estos momentos, solo se pueden utilizar productos físicos sencillos con TPV.\nOtros tipos de productos, como los variables y los virtuales, estarán disponibles en futuras actualizaciones.
+ Para aceptar el pago de un producto no sencillo, sal de TPV y crea un nuevo pedido desde la pestaña de pedidos.
¿Por qué no puedo ver mis productos?
Información
Cerrar
- Más información
+ Más\u00A0información
En estos momentos, solo los productos físicos sencillos son compatibles con TPV. Otros tipos de productos, como los variables y los virtuales, estarán disponibles en futuras actualizaciones.
Mostrar solo productos sencillos
Dirección del sitio
@@ -39,8 +98,6 @@ Language: es
Las órdenes de compra actualmente solo admiten productos simples: \ncrear uno para empezar.
No se han encontrado productos compatibles
Sin productos
- Atendamos a algunos clientes
- Puesta en marcha
Conseguir ayuda
Conectar con tu lector
Foto eliminada
@@ -135,8 +192,7 @@ Language: es
No hay reglas de cantidad
Público
Cancelar
- Salir
- ¿Estás seguro que quieres salir del punto de venta?
+ Salir
Salir del punto de venta
Eliminar %s del carrito
Finalizar compra
@@ -154,7 +210,7 @@ Language: es
Pocas existencias
ENVÍO
Comparte tu opinión
- ¿Se compra fácilmente en Woo?
+ ¿Es fácil gestionar envíos con Woo?
¡Se ha añadido el envío!
Editar envío
Editar envío
@@ -214,7 +270,7 @@ Language: es
Los análisis de sesiones se basan en los recuentos de visitantes únicos, que no están disponibles para rangos de fechas personalizados
Datos de la sesión no disponibles
No disponible
- Rendimiento
+ Estadísticas
Personalizado
Botón de cambio de rango de fechas
Las imágenes no están disponibles porque tu sitio está marcado como privado. Puedes modificar este ajuste cambiando al modo Próximamente.\n
@@ -256,7 +312,7 @@ Language: es
Paquetes
Paquetes vendidos
Campañas de Blaze
- Mejor rendimiento
+ Productos más vendidos
¿Seguro que deseas descartar los cambios realizados a este producto?
Vas a descartar los cambios en %s
La función de estadísticas no admite la visualización de los datos de visitantes y conversiones para rangos de fechas arbitrarios.\n\nSin embargo, puedes pulsar un valor del gráfico para consultar los visitantes y las conversiones de ese rango específico.
@@ -419,20 +475,16 @@ Language: es
Descripción corta
Cambiar imagen
Aplicar
- Empieza
+ Fecha de inicio
%1$s días
- Fijar duración
Las impresiones reflejan la frecuencia con la que aparece tu anuncio a los clientes potenciales.\n\n\n Aunque no se pueden garantizar cifras exactas debido a las fluctuaciones del tráfico en línea y del comportamiento de los usuarios, nuestro objetivo es que las impresiones reales de tu anuncio coincidan lo más posible con el recuento objetivo.\n\n\n No olvides que las impresiones tienen que ver con la visibilidad, no con la acción de los lectores.
Hecho
Impresiones
Actualizar
Editar
- Duración
Estimación diaria del número de personas a la que has llegado
%1$s al día
durante %1$s días
- Gasto total
- ¿Cuánto te gustaría gastar en la campaña de promoción de tu producto?
Fija tu presupuesto
Todos
%1$s días desde %2$s
@@ -634,7 +686,6 @@ Language: es
Genera más ventas hacia tu tienda con Blaze
Ha habido un error al actualizar la lista de campañas. Inténtalo de nuevo más tarde.
Elige una fuente de medios
- Ha ocurrido un error al generar el nombre y la descripción del producto.
No se ha detectado texto. Elige otra foto del embalaje o introduce los detalles del producto manualmente.
Añadir producto
Escanear código de barras
@@ -656,9 +707,6 @@ Language: es
Haz un pago de %s con tu tarjeta de débito o crédito.\nEl pago se te reembolsará cuando hayas terminado.
Es fácil, seguro y privado.
Acepta todo tipo de pagos en persona, directamente\nen tu teléfono. No se necesita ningún hardware adicional.
- Presupuesto
- Clics
- Impresiones
Rechazada
Completada
Activa
@@ -691,46 +739,23 @@ Language: es
AJUSTES
Añadir detalles de forma manual mediante el correo electrónico
No hemos podido procesar tu solicitud de acceso a la aplicación
- Se ha producido un error al copiar el nombre del producto en el portapapeles.
- Nombre del producto copiado en el portapapeles.
- Nombre del producto generado por la IA
Se ha producido un error al copiar la descripción del producto en el portapapeles.
Descripción del producto copiada en el portapapeles.
La generación del producto ha fallado. Inténtalo de nuevo
Borrar dirección y dejar de utilizar esta tasa
Establecer una nueva tasa de impuesto para este pedido
Añadir automáticamente la tasa de impuesto
- Se ha producido un problema al generar el nombre del producto. Inténtalo de nuevo.
- Regenerar
- Escríbelo por mí
- Cuéntanos en qué consiste tu producto y qué lo hace único.
- Deja que la IA genere títulos cautivadores por ti
- Nombre del producto
Prueba a desactivar el filtro de no leídos para ver todas las reseñas de tus productos
No hay reseñas de productos no leídas
- Tono seleccionado
Convincente
Floral
Formal
Casual
- Establece el tono y la voz para dar forma a una presentación de tu producto que se ajuste a tu marca.
Tono y voz
Detalles
- Descripción del producto
Nombre del producto
- Siempre puedes cambiar los detalles de abajo más adelante.
Vista previa
- Crear detalles del producto
- Establecer tono y voz
- Añade características, ventajas o detalles clave para ayudar a que tu producto se encuentre en Internet.
Por ejemplo, tejido suave, costuras duraderas, diseño único
- Destaca lo que hace que tu producto sea único y deja que la IA haga la magia.
- Acerca de tu producto
- Sugiere un nombre
- Por ejemplo: tejido suave, costuras duraderas, diseño único.
- Nombre del producto
- O amplía tus opciones pulsando para obtener más sugerencias de nombres.
- Añada el nombre de tu producto
Con tecnología de IA. <a href=\'guidelines\'><u>Más información</u></a>.
Añadir un producto y los detalles manualmente
Añadir manualmente
@@ -742,8 +767,8 @@ Language: es
Esto no afectará a los pedidos en línea
Añadir este tipo a todos los pedidos creados
Editar tipos impositivos
- Editar tipos impositivos en el escritorio
- Añade tipos impositivos al escritorio. Solo se mostrarán aquí los tipos impositivos con información sobre la ubicación.
+ Editar tipos impositivos en wp-admin
+ Añade tipos impositivos en wp-admin. Solo se mostrarán aquí los tipos impositivos con información sobre la ubicación.
No hemos encontrado tipos impositivos
Descubre otros proveedores de pago y \nelige un proveedor de pago.
Métodos de pago
@@ -1054,8 +1079,7 @@ Language: es
Aumentar las ventas con ofertas especiales
Ver tu tienda
Mantengase actualizado
- Únete a los pagos a través de móvil
- Gestionar más en la administración
+ Gestionar más en wp-admin
General
Ajustes
Puedes editar productos agrupados en el escritorio del sitio web.
@@ -2342,7 +2366,7 @@ Language: es
Ajustes de la tienda
Atributos
Puedes reembolsar %1$s
- Recepción de pago
+ Recibir pago
y
¿Eliminar este atributo?
Cualquiera
@@ -3354,7 +3378,7 @@ Language: es
No se ha podido compartir el registro
Se ha producido un error al copiar en el portapapeles
Se ha copiado el registro en el portapapeles
- Registro de solicitud
+ Ver logs de la aplicación
Nuevo mensaje de \"Ayuda y soporte técnico\"
WooCommerce
No se ha configurado
diff --git a/WooCommerce/src/main/res/values-fr/strings.xml b/WooCommerce/src/main/res/values-fr/strings.xml
index faefd474014..c0c799c747d 100644
--- a/WooCommerce/src/main/res/values-fr/strings.xml
+++ b/WooCommerce/src/main/res/values-fr/strings.xml
@@ -1,11 +1,71 @@
+ Clé non valide : veuillez supprimer le caractère « _ » se trouvant au début.
+ Cette clé est déjà utilisée pour un autre champ personnalisé.\nL’application ne prend actuellement pas en charge la création de clés dupliquées. Veuillez utiliser WP Admin pour dupliquer une clé si nécessaire.
+ Ajouter des champs personnalisés
+ Champ personnalisé supprimé
+ Échec de l’enregistrement des modifications, veuillez réessayer
+ Modifications enregistrées
+ Enregistrement des modifications
+ Il semblerait que vous ne soyez pas connecté(e) à Internet. Assurez-vous que votre Wi-Fi est activé. Si vous utilisez des données mobiles, assurez-vous qu’elles sont activées dans les réglages de votre appareil.
+ Échec du scan. Veuillez réessayer plus tard
+ Valeur
+ Clé
+ D’autres types de produits, tels que les produits virtuels et variables, seront disponibles dans les prochaines mises à jour.
+ Seuls les produits physiques simples peuvent être utilisés actuellement avec le PDV.
+ Annuler
+ Durée
+ La campagne durera jusqu’à ce que vous y mettiez un terme.
+ Préciser la durée
+ jusqu’au %1$s
+ Planifier
+ Dépense quotidienne
+ Combien souhaitez-vous dépenser pour votre campagne et combien de temps doit-elle durer ?
+ %1$s ➔ %2$s
+ Faites découvrir vos produits à des millions de personnes et stimulez vos ventes avec Blaze.
+ Vous souhaitez stimuler vos ventes ?
+ Erreur lors du chargement des champs personnalisés
+ Champs personnalisés
+ Arrière-plan peu éclairé. Appuyez pour ignorer la fenêtre de dialogue.
+ %1$s par semaine
+ Exécuter jusqu’à ce que je l’arrête
+ À partir du %1$s
+ dépense hebdomadaire
+ %1$s par semaine à partir du %2$s
+ Hebdomadaire
+ Restant
+ Total
+ Nombre de clics
+ Il semblerait que votre appareil soit en mode Économie d’énergie. \nNous ne pouvons pas fournir d’informations sur votre boutique tant qu’il est activé.
+ Menu contextuel avec options. Balayez pour naviguer parmi les éléments.
+ Ouvrir le menu de la barre d’outils
+ Barre d’outils avec l’état du lecteur de carte. Le menu est ouvert. Appuyez deux fois pour interagir.
+ Barre d’outils avec l’état du lecteur de carte. Appuyez deux fois pour interagir.
+ Menu désactivé
+ Menu activé
+ Lecteur de carte non connecté. Appuyer deux fois pour le connecter
+ Lecteur de carte connecté
+ Arrière-plan peu éclairé. Appuyez pour fermer le menu.
+ Icône en forme de coche Paiement effectué
+ Retirer cet élément du panier
+ Toutes les commandes en cours seront perdues.
+ Quitter le mode point de vente ?
+ Fermer
+ Arrière-plan peu éclairé. Appuyez pour ignorer la fenêtre de dialogue.
+ Appuyez deux fois pour ignorer la fenêtre de dialogue
+ Fenêtre de dialogue Produits simples uniquement
+ Appuyer deux fois pour lire la suite
+ Bannière Produits simples uniquement
+ Faites la promotion de vos produits avec les publicités Blaze et augmentez vos ventes dès maintenant.
+ Dynamiser vos ventes
+ Accepter les paiements en mode nomade
+ Un code PIN incorrect a été saisi. Réessayer ou utiliser un autre moyen de paiement
Menu
Produit dans le panier %s, Prix %s
Produit %s, Prix %s
@@ -14,12 +74,11 @@ Language: fr
Nouvelle commande
OK
+ Créer une commande dans la gestion de la boutique
- Pour accepter un paiement pour un produit non simple, quittez le PDV et créez une nouvelle commande depuis l’onglet Commandes.
- Seuls les produits physiques simples peuvent être utilisés avec le PDV actuellement.\nD’autres types de produits, tels que les produits virtuels et variables, seront disponibles dans les mises à jour à venir.
+ Pour accepter un paiement pour un produit non simple, quittez le PDV et créez une nouvelle commande depuis l’onglet Commandes.
Pourquoi ne puis-je pas voir mes produits ?
Informations
Fermer
- Lire la suite
+ En savoir plus
Seuls les produits physiques simples sont compatibles avec le PDV actuellement. D’autres types de produits, tels que les produits virtuels et variables, seront disponibles dans les mises à jour à venir.
Affichage des produits simples uniquement
Adresse du site
@@ -39,8 +98,6 @@ Language: fr
Le PDV ne prend actuellement en charge que les produits simples – \ncréez-en un pour commencer.
Aucun produit pris en charge trouvé
Aucun produit
- Servir davantage de clients
- Démarrage
Obtenir de l’aide
Se connecter à votre lecteur
Photo supprimée
@@ -135,8 +192,7 @@ Language: fr
Pas de règles de quantité
Audience
Annuler
- Quitter
- Voulez-vous vraiment quitter le PDV ?
+ Quitter
Quitter le PDV
Retirer %s du panier
Validation de la commande
@@ -419,20 +475,16 @@ Language: fr
Description
Changer l’image
Appliquer
- Début
+ Date de début
%1$s jours
- Fixer la durée
Les impressions reflètent la fréquence à laquelle votre publicité apparaît pour les clients potentiels.\n\n\n Il est impossible de donner les chiffres exacts en raison de la fluctuation du trafic en ligne et des comportements des internautes, nous visons à faire correspondre autant que possible les véritables impressions à votre nombre cible.\n\n\n Rappelez-vous, les impressions sont une affaire de visibilité, pas d’action entreprise par les internautes.
Terminé
Impressions
Mettre à jour
Modifier
- Durée
Estimation du nombre de personnes atteintes par jour
%1$s par jour
pendant %1$s jours
- Dépense totale
- Combien souhaiteriez-vous dépenser pour votre campagne promotionnelle ?
Définir votre budget
Tout
%1$s jours à partir de %2$s
@@ -634,7 +686,6 @@ Language: fr
Augmenter les ventes de votre boutique avec Blaze
Une erreur s’est produite lors de l’actualisation de la liste de campagnes. Veuillez réessayer plus tard.
Sélectionner une source multimédia
- Une erreur est survenue lors de la génération du nom et de la description du produit.
Aucun texte détecté. Sélectionnez une autre photo de l’emballage ou entrez manuellement les détails du produit.
Ajouter un produit
Scanner le code-barres
@@ -656,9 +707,6 @@ Language: fr
Essayez de procéder à un paiement %s avec votre carte de débit ou de crédit.\nLe paiement sera remboursé lorsque vous aurez terminé.
C’est facile, sûr et privé.
Acceptez tous les types de paiements en personne directement\nsur votre téléphone. Aucun matériel supplémentaire nécessaire.
- Budget
- Clics
- Impressions
Refusée
Terminée
Active
@@ -691,46 +739,23 @@ Language: fr
RÉGLAGES
Ajouter des détails manuellement avec l’adresse e-mail
Nous n’avons pas pu traiter votre demande de connexion à l’application
- Erreur lors de la copie du nom du produit dans le presse-papiers.
- Nom du produit copié dans le presse-papiers.
- Nom de produit généré par l’IA
Erreur lors de la copie de la description du produit dans le presse-papiers.
Description du produit copiée dans le presse-papiers.
Le produit n’a pas pu être généré. Veuillez réessayer
Effacer l’adresse et arrêter d’utiliser ce taux
Définir un nouveau taux de taxation pour cette commande
Ajout automatique du taux de taxation
- Un problème est survenu lors de la génération du nom du produit. Veuillez réessayer.
- Régénérer
- L’écrire à ma place
- Présentez-nous votre produit et dites-nous ce qui le rend unique !
- Laissez l’IA générer des titres captivants pour vous
- Nom du produit
Essayer de désactiver le filtre des avis non lus pour afficher tous les avis sur les produits
Aucun avis non lu sur les produits
- Ton sélectionné
Convaincant
Fleuri
Formel
Décontracté
- Définissez le ton et la voix pour que la présentation de votre produit corresponde à votre marque.
Ton et voix
Détails
- Description du produit
Nom du produit
- Vous pourrez toujours modifier les détails ci-après ultérieurement.
Aperçu
- Créer les détails du produit
- Définir le ton et la voix
- Ajoutez les fonctionnalités, les avantages ou les détails clés pour faciliter la recherche de votre produit en ligne.
Par exemple : tissu doux, coutures solides, design unique
- Mettez en avant ce qui rend votre produit unique et laissez l’IA s’occuper du reste.
- À propos de votre produit
- Proposer un nom
- Par exemple : tissu doux, coutures solides, design unique.
- Nom du produit
- Vous pouvez aussi obtenir d’autres suggestions de noms en un clic.
- Ajouter le nom de votre produit
Alimenté par l’IA. <a href=\'guidelines\'><u>Lire la suite</u></a>.
Ajouter un produit et les détails manuellement
Ajouter manuellement
@@ -1054,7 +1079,6 @@ Language: fr
Accélérez les ventes grâce à des offres spéciales
Voir votre boutique
Restez à jour
- Associez les paiements sur mobile
Gérez plus de paramètres d’administration
Général
Réglages
diff --git a/WooCommerce/src/main/res/values-he/strings.xml b/WooCommerce/src/main/res/values-he/strings.xml
index a5691d5c71f..9c41288e6fc 100644
--- a/WooCommerce/src/main/res/values-he/strings.xml
+++ b/WooCommerce/src/main/res/values-he/strings.xml
@@ -1,11 +1,71 @@
+ מפתח לא תקין: יש להסיר את התו \"_\" מההתחלה.
+ כבר השתמשת במפתח הזה בשדה מותאם אחר.\nהאפליקציה לא תומכת כרגע ביצירה של מפתחות כפולים. אם צריך, יש להשתמש ב\'ניהול WP\' כדי לשכפל מפתח.
+ להוסיף שדה מותאם
+ השדה המותאם נמחק
+ שמירת השינויים נכשלה, יש לנסות שוב
+ השינויים נשמרו
+ שומר שינויים
+ נראה שהחיבור לאינטרנט לא פעיל אצלך. יש לוודא שהחיבור ל-Wi-Fi הופעל. אם ברצונך להשתמש בנתונים הניידים, עליך לוודא שהאפשרות הופעלה בהגדרות המכשיר.
+ הסריקה נכשלה. יש לנסות שוב מאוחר יותר
+ ערך
+ מפתח
+ סוגי מוצרים אחרים, כמו מוצרים עם סוגים או מוצרים וירטואליים, יהיו זמינים בעדכונים בעתיד.
+ נכון לעכשיו, ניתן להשתמש ב-POS רק עבור מוצרים פיזיים פשוטים.
+ ביטול
+ משך זמן
+ הקמפיין יהיה פעיל עד שיושבת על ידיך.
+ יש לציין משך
+ עד %1$s
+ תזמון
+ הוצאה יומית
+ כמה ברצונך להוציא על הקמפיין וכמה זמן ברצונך להפעיל אותו?
+ %1$s ➔ %2$s
+ בעזרת Blaze, מיליונים יראו את המוצרים שלך והמכירות ישתפרו
+ רוצה לשפר את המכירות שלך?
+ אירעה שגיאה בעת טעינת שדות מותאמים
+ שדות מותאמים
+ רקע מעומעם. יש להקיש כדי לצאת מתיבת הדו-שיח.
+ %1$s בכל שבוע
+ להמשיך בפעולה עד שאעצור אותה
+ פעיל ברצף מ-%1$s
+ הוצאה שבועית
+ חיוב שבועי של %1$s, החל מ-%2$s
+ שבועית
+ נשאר
+ סכום כולל
+ קליקים
+ נראה שהמכשיר שלך נמצא במצב \'חיסכון בסוללה\'. \nאנחנו לא יכולים לספק פרטים על החנות שלך כאשר המצב הזה פעיל
+ תפריט קופץ עם אפשרויות. יש להחליק כדי לנווט בין הפריטים.
+ לפתוח את התפריט של סרגל הכלים
+ סרגל כלים עם מצב של קורא כרטיסים. התפריט פתוח. יש להקיש הקשה כפולה לאינטראקציה.
+ סרגל כלים עם מצב של קורא כרטיסים. יש להקיש הקשה כפולה לאינטראקציה.
+ התפריט הושבת
+ התפריט הופעל
+ קורא הכרטיסים לא מחובר. יש להקיש הקשה כפולה כדי להתחבר
+ קורא הכרטיסים מחובר
+ רקע מעומעם. יש להקיש כדי לסגור את התפריט.
+ סמל אישור שמציין את הצלחת התשלום
+ להסיר את הפריט מעגלת הקניות
+ כל התקדמות שנרשמה בהזמנות תאבד.
+ האם לצאת ממצב \'נקודת מכירה\'?
+ לסגור
+ רקע מעומעם. יש להקיש כדי לצאת מתיבת הדו-שיח.
+ יש להקיש הקשה כפולה כדי לצאת מתיבת הדו-שיח
+ תיבת דו-שיח של מוצרים פשוטים בלבד
+ יש להקיש הקשה כפולה למידע נוסף
+ באנר של מוצרים פשוטים בלבד
+ לקדם מוצרים עם פרסומות של Blaze כדי להגדיל את המכירות שלך עכשיו.
+ לשפר את המכירות
+ לגבות תשלומים מכל מקום
+ הוזן קוד PIN שגוי. יש לנסות שוב או להשתמש באמצעי תשלום אחר
תפריט
מוצר בעגלת הקניות %s, מחיר %s
מוצר %s, מחיר %s
@@ -14,12 +74,11 @@ Language: he_IL
הזמנה חדשה
אישור
+ ליצור הזמנה בניהול החנות
- כדי לגבות תשלום במוצר שאינו מוצר פשוט, יש לצאת מ-POS וליצור הזמנה חדשה מלשונית ההזמנות.
- נכון לעכשיו, ניתן להשתמש ב-POS רק עבור מוצרים פיזיים פשוטים.\nסוגי מוצרים אחרים, כמו מוצרים עם סוגים או מוצרים וירטואליים, יהיו זמינים בעדכונים בעתיד.
+ כדי לגבות תשלום במוצר שאינו מוצר פשוט, יש לצאת מ-POS וליצור הזמנה חדשה מלשונית ההזמנות.
למה המוצרים שלי לא מופיעים?
מידע
לסגור
- למידע נוסף
+ למידע\u00A0נוסף
רק מוצרים פיזיים פשוטים תואמים ל-POS כרגע. סוגי מוצרים אחרים, כמו מוצרים עם סוגים או מוצרים וירטואליים, יהיו זמינים בעדכונים בעתיד.
מציג רק מוצרים פשוטים
כתובת האתר
@@ -39,8 +98,6 @@ Language: he_IL
האפשרות POS נתמכת כרגע במוצרים פשוטים בלבד – \nיש ליצור אחד כזה כדי להתחיל.
לא נמצאו מוצרים נתמכים
אין מוצרים
- זה הזמן להעניק ללקוחות שירות
- מתחילים כאן
לקבלת תמיכה
לחבר את הקורא
התמונה הוסרה
@@ -135,8 +192,7 @@ Language: he_IL
אין כללי כמות
קהל
ביטול
- יציאה
- האם ברצונך לצאת מ-POS?
+ יציאה
לצאת מ-POS
להסיר את %s מעגלת הקניות
תשלום בקופה
@@ -419,20 +475,16 @@ Language: he_IL
תיאור האתר
להחליף תמונה
להחיל
- מתחיל במועד
+ תאריך התחלה
%1$s ימים
- להגדיר מיקום
הצפיות מייצגות את התדירות שבה הפרסומת שלך מוצגת ללקוחות פוטנציאליים.\n\n\n אומנם לא ניתן להבטיח שהמספרים יהיו מדויקים עקב שינויים בתעבורה המקוונת והתנהגות המשתמשים, אך אנחנו שואפים להתאים כמה שאפשר את מספר הצפיות במודעה שלך בפועל למספר היעד שהצבת.\n\n\n חשוב לזכור, הצפיות משקפות את הנראות של הפרסומת, לא את הפעולה שבוצעה על ידי הצופים.
סיימתי
צפיות
לעדכן
לערוך
- משך זמן
מספר האנשים שיראו את התוכן בכל יום
%1$s בכל יום
ל-%1$s ימים
- הוצאה כוללת
- כמה ברצונך להוציא על קמפיין הקידום של המוצר שלך?
להגדיר את תקציב שלך
הכול
%1$s ימים מ-%2$s
@@ -634,7 +686,6 @@ Language: he_IL
להגביר מכירות בחנות שלך בעזרת Blaze
שגיאה בעת ריענון רשימת הקמפיינים. נא לנסות שוב מאוחר יותר.
לבחור מקור מדיה
- שגיאה בעת יצירת שם ותיאור מוצר.
לא זוהה טקסט. יש לבחור תמונת אריזה אחרת או להזין את פרטי המוצר באופן ידני.
להוסיף מוצר
לסרוק ברקוד
@@ -656,9 +707,6 @@ Language: he_IL
לנסות תשלום של %s באמצעות כרטיס החיוב או כרטיס האשראי.\nהתשלום יוחזר בסיום.
התהליך קל, מאובטח ופרטי.
לקבל את כל הסוגים של תשלומים באופן אישי, ישר\nבטלפון. ללא צורך בחומרה נוספת.
- תקציב
- קליקים
- צפיות
נדחה
הושלם
פעיל
@@ -691,46 +739,23 @@ Language: he_IL
הגדרות
להוסיף פרטים ידנית באמצעות אימייל
לא הצלחנו לעבד את בקשת הכניסה לאפליקציה
- שגיאה בעת ההעתקה של שם המוצר ללוח.
- שם המוצר הועתק ללוח.
- שם המוצר נוצר על ידי בינה מלאכותית
שגיאה בעת ההעתקה של תיאור המוצר ללוח.
תיאור המוצר הועתק ללוח.
יצירת המוצר נכשלה. יש לנסות שוב
למחוק את הכתובת ולהפסיק להשתמש בשיעור המס הזה
לקבוע שיעור מס חדש להזמנה זו
הוספה אוטומטית של שיעור מס
- אירעה בעיה ביצירה של שם המוצר. יש לנסות שוב.
- ליצור מחדש
- לכתוב עבורי
- נשמח לשמוע מה המוצר שברצונך למכור ומה מייחד אותו!
- הבינה המלאכותית יכולה ליצור עבורך שמות מעניינים
- שם המוצר
כדי להציג את כל הביקורות שהתקבלו למוצר, נסו להשבית את המסנן של פריטים שלא נקראו
אין ביקורות שלא נקראו למוצרים
- טון הדיבור שנבחר
משכנע
מתחכם
רשמי
יומיומי
- כדאי להגדיר את הנימה ואת טון הדיבור כדי לוודא שהתצוגה של המוצר מתאימה למותג שלך.
נימה וטון דיבור
פרטים
- תיאור המוצר
שם המוצר
- תמיד אפשר לשנות את הפרטים שלמטה מאוחר יותר.
תצוגה מקדימה
- ליצור את פרטי המוצר
- להגדיר את הנימה וטון הדיבור
- יש להוסיף מאפיינים מרכזיים, יתרונות או פרטים שיעזרו למחפשים למצוא את המוצר שלך באינטרנט.
למשל: בד רך, תפירה חזקה, עיצוב ייחודי
- ניתן להדגיש את המאפיינים שמייחדים את המוצר שלך ולהשתמש בקסמיה של הבינה המלאכותית כדי ליצור שמות.
- פרטים לגבי המוצר
- להציע שם
- למשל: בד רך, תפירה חזקה, עיצוב ייחודי.
- שם המוצר
- לחלופין, ניתן להרחיב את הבחירה על ידי הקשה על הצעות לשמות נוספים.
- לציין את שם המוצר
מופעל באמצעות בינה מלאכותית. <a href=\'guidelines\'><u>למידע נוסף</u></a>.
להוסיף מוצר ואת הפרטים באופן ידני
להוסיף ידנית
@@ -1054,7 +1079,6 @@ Language: he_IL
לשפר את שיעור המכירה עם מבצעים מיוחדים
הצגת החנות שלך
להישאר בעניינים
- להצטרף לתשלומים מהנייד
לנהל אפשרויות נוספות באזור הניהול
כללי
הגדרות
diff --git a/WooCommerce/src/main/res/values-id/strings.xml b/WooCommerce/src/main/res/values-id/strings.xml
index f65de263220..b91c73ee2d6 100644
--- a/WooCommerce/src/main/res/values-id/strings.xml
+++ b/WooCommerce/src/main/res/values-id/strings.xml
@@ -1,11 +1,71 @@
+ Kunci tidak valid: hapus karakter \"_\" dari awal.
+ Kunci ini sudah digunakan untuk kolom kustom yang lain. \nSaat ini aplikasi tidak mendukung pembuatan kunci duplikat. Bila perlu, gunakan wp-admin untuk membuat duplikat kunci.
+ Tambahkan kolom khusus
+ Perubahan tersimpan
+ Menyimpan perubahan
+ Tampaknya Anda tidak terhubung ke internet. Pastikan Wi-Fi Anda aktif. Jika Anda menggunakan data seluler, aktifkan di pengaturan perangkat Anda.
+ Pemindaian gagal. Coba lagi nanti
+ Kolom Kustom telah dihapus
+ Gagal menyimpan perubahan, coba lagi
+ Nilai
+ Kunci
+ Jenis produk lain, seperti variabel dan virtual, akan tersedia dalam pembaruan mendatang.
+ Saat ini, hanya produk fisik sederhana yang dapat menggunakan POS.
+ Batal
+ Durasi
+ Kampanye akan terus berjalan sampai Anda menghentikannya.
+ Tentukan durasi
+ menjadi %1$s
+ Jadwal
+ Pengeluaran per hari
+ Berapa banyak dana yang ingin Anda belanjakan untuk kampanye, dan berapa lama durasinya?
+ %1$s ➔ %2$s
+ Ingin meningkatkan penjualan Anda?
+ Perlihatkan produk Anda kepada jutaan orang dengan Blaze dan tingkatkan penjualan Anda
+ Error saat memuat kolom khusus
+ Kolom Khusus
+ Latar belakang diredupkan. Ketuk untuk menutup dialog.
+ %1$s per minggu
+ Jalankan terus sampai saya nonaktifkan
+ %1$s per minggu, mulai dari %2$s
+ Per minggu
+ Tersisa
+ Total
+ Klik-tayang
+ Sepertinya perangkat Anda dalam mode Penghemat Baterai. \nKami tidak dapat memberikan informasi toko selama mode ini aktif.
+ Berjalan sejak %1$s
+ pengeluaran per minggu
+ Menu popup yang memuat sejumlah opsi. Geser untuk mengakses aneka item.
+ Buka menu bilah alat
+ Bilah alat dengan status pembaca kartu. Menu terbuka. Ketuk dua kali untuk berinteraksi.
+ Bilah alat dengan status pembaca kartu. Ketuk dua kali untuk berinteraksi.
+ Menu Dinonaktifkan
+ Menu Diaktifkan
+ Pembaca kartu tidak terhubung. Ketuk dua kali untuk menghubungkan
+ Pembaca kartu terhubung
+ Latar belakang diredupkan. Ketuk untuk menutup menu.
+ Ikon centang untuk pembayaran berhasil
+ Hapus item ini dari keranjang
+ Semua pesanan yang sedang aktif akan hilang.
+ Keluar dari mode Point of Sale?
+ Tutup
+ Latar belakang diredupkan. Ketuk untuk menutup dialog.
+ Ketuk dua kali untuk menutup dialog
+ Dialog khusus produk sederhana
+ Ketuk dua kali untuk melihat selengkapnya
+ Banner khusus produk sederhana
+ Promosikan produk Anda dengan Iklan Blaze dan dongkrak penjualan sekarang.
+ Tingkatkan penjualan Anda
+ Terima pembayaran di mana saja
+ PIN yang dimasukkan salah. Coba lagi, atau gunakan metode pembayaran lainnya
Menu
Produk di keranjang %s, Harga %s
Produk %s, Harga %s
@@ -14,14 +74,13 @@ Language: id
Pesanan baru
Oke
+ Buat pesanan di manajemen toko
- Untuk memproses pembayaran atas produk non-sederhana, keluar dari POS, kemudian buat pesanan baru dari tab pesanan.
Mengapa produk saya tidak muncul?
Info
Tutup
- Baca selengkapnya
Menunjukkan produk sederhana saja
- Saat ini, hanya produk fisik sederhana yang dapat menggunakan POS.\nJenis produk lain, seperti variabel dan virtual, akan tersedia dalam pembaruan mendatang.
Saat ini, hanya produk fisik sederhana yang kompatibel dengan POS. Jenis produk lain, seperti variabel dan virtual, akan tersedia dalam pembaruan mendatang.
+ Untuk memproses pembayaran atas produk non-sederhana, keluar dari POS, kemudian buat pesanan baru dari tab pesanan.
+ Pelajari\u00A0selengkapnya
Alamat Situs
Tambahkan kampanye berbayar
Tingkatkan penjualan dan tarik lebih banyak pengunjung dengan Google Ads.
@@ -39,8 +98,6 @@ Language: id
Coba lagi
Produk yang didukung tidak ditemukan
Tidak ada produk
- Mari layani pelanggan
- Memulai
Dapatkan Dukungan
Hubungkan ke pembaca
Foto dihapus
@@ -135,11 +192,10 @@ Language: id
Pajak
Audiens
Batal
- Keluar
- Anda yakin ingin keluar dari POS?
Keluar dari POS
Checkout
Hapus %s dari keranjang
+ Keluar
Status Pembaca Tidak Diketahui
Checkout
Pembaca Terhubung
@@ -419,23 +475,19 @@ Language: id
Slogan
Ganti gambar
Terapkan
- Mulai
%1$s hari
- Tentukan durasi
- Tayangan menunjukkan frekuensi iklan ditampilkan kepada calon pembeli.\n\n\n Meskipun jumlah persisnya tidak dapat dijamin karena fluktuasi lalu lintas online dan perilaku pengguna, kami berupaya agar tayangan aktual iklan Anda sedekat mungkin dengan jumlah yang Anda targetkan.\n\n\n Jangan lupa, tayangan berhubungan dengan visibilitas, bukan tindakan yang dilakukan pembaca.
Selesai
Tayangan
Perbarui
Edit
- Durasi
Estimasi orang yang dijangkau per hari
%1$s harian
selama %1$s hari
- Total pengeluaran
- Berapa banyak dana yang ingin Anda keluarkan untuk kampanye promosi produk?
Tentukan anggaran
Semua
%1$s hari sejak %2$s
+ Tayangan menunjukkan frekuensi iklan ditampilkan kepada calon pembeli.\n\n\n Meskipun jumlah persisnya tidak dapat dijamin karena fluktuasi lalu lintas online dan perilaku pengguna, kami berupaya agar tayangan aktual iklan Anda sedekat mungkin dengan jumlah yang Anda targetkan.\n\n\n Jangan lupa, tayangan berhubungan dengan visibilitas, bukan tindakan yang dilakukan pembaca.
+ Tanggal mulai
Jangan Tampilkan Lagi
Ingatkan Saya Nanti
Boleh minta waktunya sebentar? Bantu kami memperbaiki fitur berbantuan AI dengan feedback cepat.
@@ -635,7 +687,6 @@ Language: id
Tidak ada teks yang terdeteksi. Pilih foto kemasan lainnya atau masukkan detail produk secara manual.
Tingkatkan penjualan toko Anda dengan Blaze
Terjadi error ketika menyegarkan daftar kampanye. Silakan coba lagi nanti.
- Terjadi error ketika membuat nama & deskripsi produk.
Tambahkan produk
Pindai barcode
Ciutkan/perluas kartu produk
@@ -656,9 +707,6 @@ Language: id
Coba lakukan pembayaran %s dengan kartu debit atau kredit Anda.\nDana akan dikembalikan saat selesai.
Mudah, aman, privasi terjaga.
Terima semua jenis pembayaran fisik, langsung\ndari ponsel. Tidak perlu alat tambahan.
- Anggaran
- Klik
- Tayangan
Ditolak
Selesai
Aktif
@@ -691,45 +739,23 @@ Language: id
PENGATURAN
Tambahkan detail secara manual dengan email
Kami tidak dapat memproses permintaan login aplikasi Anda
- Error saat menyalin nama produk ke clipboard.
- Nama produk disalin ke clipboard.
- Nama produk dibuat oleh AI
Error saat menyalin deskripsi produk ke clipboard.
Deskripsi produk disalin ke clipboard.
Pembuatan produk tidak berhasil. Coba lagi
Hapus alamat dan hentikan penggunaan tarif pajak ini
Tentukan tarif pajak baru untuk pesanan ini
Tarif pajak ditambahkan otomatis
- Terjadi masalah saat membuat nama produk. Coba lagi.
- Minta tuliskan
- Ceritakan tentang produk dan keunikannya.
- AI akan membuatkan Anda judul yang menarik
- Buat ulang
Coba nonaktifkan penyaring belum dibaca untuk melihat semua ulasan produk
- Nama produk
Tidak ada ulasan produk belum dibaca
- Nada dipilih
Persuasif
Berbunga-bunga
Formal
Kasual
- Tentukan nada dan suara agar presentasi produk sesuai dengan citra yang ingin diangkat.
Nada dan suara
Rincian
- Deskripsi produk
Nama produk
- Anda bisa mengubah detail berikut kapan saja.
Pratinjau
- Buat Info Produk
- Tentukan nada dan suara
- Tambahkan fitur utama, keunggulan, atau informasi produk agar mudah ditemukan di internet.
Misalnya, kain lembut, jahitan awet, desain unik
- Tonjolkan keunikan produk dan biar AI yang urus selanjutnya.
- Tentang produk
- Sarankan nama
- Misalnya: Kain lembut, jahitan awet, desain unik.
- Nama produk
- Masukkan nama produk
Berbasis AI. <a href=\'guidelines\'><u>Baca selengkapnya</u></a>.
Tambahkan produk dan informasi secara manual
Tambahkan secara manual
@@ -738,7 +764,6 @@ Language: id
Tambahkan produk
Hanya ulasan belum dibaca
Edit Pengaturan Tarif Pajak
- Atau, ketuk untuk mendapatkan saran nama lainnya.
Hal ini tidak akan memengaruhi pesanan online
Tambahkan tarif ini ke semua pesanan yang dibuat
Sunting Tarif Pajak
@@ -1054,7 +1079,6 @@ Language: id
Tingkatkan penjualan dengan penawaran khusus
Lihat toko Anda
Pantau selalu
- Aktifkan pembayaran dengan perangkat seluler
Kelola lebih lanjut di admin
Umum
Pengaturan
diff --git a/WooCommerce/src/main/res/values-it/strings.xml b/WooCommerce/src/main/res/values-it/strings.xml
index 2cb2b38d986..6b847bc1fc5 100644
--- a/WooCommerce/src/main/res/values-it/strings.xml
+++ b/WooCommerce/src/main/res/values-it/strings.xml
@@ -1,11 +1,71 @@
+ Chiave non valida: rimuovere il carattere \"_\" dall\'inizio.
+ Questa chiave è già utilizzata in un altro campo personalizzato.\nAttualmente l\'applicazione non supporta la creazione di chiavi duplicate. Usare wp-admin per duplicare una chiave, se necessario.
+ Aggiungi campi personalizzati
+ Campo personalizzato eliminato
+ Impossibile salvare le modifiche, riprovare
+ Modifiche salvate
+ Salvataggio delle modifiche in corso
+ Sembra non ci sia connessione a Internet. Assicurati che il Wi-Fi sia acceso. Se stai utilizzando i dati mobili, accertati che siano abilitati nelle impostazioni del dispositivo.
+ Scansione non riuscita. Riprova più tardi
+ Valore
+ Chiave
+ Altri tipi di prodotti, come quelli variabili e quelli virtuali, saranno disponibili in seguito a futuri aggiornamenti.
+ Al momento solo i prodotti fisici semplici possono essere utilizzati con POS.
+ Annulla
+ Durata
+ La campagna proseguirà finché non la interromperai.
+ Specifica la durata
+ a %1$s
+ Pianifica
+ Spesa giornaliera
+ Quanto vorresti pagare per la campagna e quanto tempo desideri che duri?
+ %1$s ➔ %2$s
+ Con Blaze i tuoi prodotti saranno visti da milioni di persone e le vendite aumenteranno
+ Stai pensando di aumentare le tue vendite?
+ Errore durante il caricamento dei campi personalizzati
+ Campi personalizzati
+ Sfondo oscurato. Tocca per ignorare la finestra di dialogo.
+ %1$s settimanali
+ Esegui fino a nuova interruzione
+ %1$s settimanali a partire dal giorno %2$s
+ Settimanale
+ Rimanente
+ Totale
+ Click-through
+ Il tuo dispositivo è in modalità Risparmio batteria. \nNon possiamo fornirti le informazioni sul tuo negozio quando questa impostazione è attivata
+ In corso dal giorno %1$s
+ spesa settimanale
+ Menu popup con opzioni. Scorri per navigare tra gli elementi.
+ Apri il menu della barra degli strumenti
+ Barra degli strumenti con lo stato del lettore delle carte Il menu è aperto. Tocca due volte per interagire.
+ Barra degli strumenti con lo stato del lettore delle carte Tocca due volte per interagire.
+ Il menu è disabilitato
+ Il menu è abilitato
+ Il lettore carte non è connesso. Tocca due volte per connettere
+ Lettore di carte connesso
+ Sfondo oscurato. Tocca per chiudere il menu.
+ Icona di pagamento avvenuto con successo
+ Rimuovi questo elemento dal carrello
+ Gli ordini in corso andranno persi.
+ Uscire dalla modalità punto vendita?
+ Chiudi
+ Sfondo oscurato. Tocca per ignorare il dialogo.
+ Doppio tocco per ignorare il dialogo
+ Dialogo solo prodotti semplici
+ Tocca due volte per scoprire di più
+ Banner solo prodotti semplici
+ Promuovi i tuoi prodotti con Blaze Ads e aumenta subito le tue vendite.
+ Aumenta le tue vendite
+ Ricevi pagamenti ovunque ti trovi
+ È stato inserito un PIN errato. Riprova o usa altri mezzi di pagamento
Menu
Prodotto nel carrello %s, Prezzo %s
Prodotto %s, Prezzo %s
@@ -14,14 +74,13 @@ Language: it
Nuovo ordine
OK
+ Crea un ordine nella gestione del negozio
- Per ricevere il pagamento di un prodotto non semplice, esci da POS e crea un nuovo ordire dalla scheda degli ordini.
- Al momento solo i prodotti fisici semplici possono essere utilizzati con POS.\nAltri tipi di prodotti, come quelli variabili e quelli virtuali, saranno disponibili in seguito a futuri aggiornamenti.
Perché non riesco a visualizzare i miei prodotti?
Informazioni
Chiudi
- Scopri di più
Al momento solo i prodotti fisici semplici sono compatibili con POS. Altri tipi di prodotti, come quelli variabili e quelli virtuali, saranno disponibili in seguito a futuri aggiornamenti.
Visualizzazione dei soli prodotti semplici
+ Per ricevere il pagamento di un prodotto non semplice, esci da POS e crea un nuovo ordire dalla scheda degli ordini.
+ Scopri di più
Indirizzo del sito
Google per WooCommerce
Aggiungi campagna a pagamento
@@ -31,16 +90,14 @@ Language: it
La tua nuova campagna è stata creata. Grandi notizie per le tue vendite.
Tutto pronto!
Impossibile creare l\'ordine
- Riprova
Icona che indica un errore
Vuoi fare un altro tentativo?
Errore durante il caricamento dei prodotti
Il punto vendita al momento supporta solo prodotti semplici
Il punto vendita al momento supporta solo prodotti semplici: \ncreane uno per iniziare.
+ Riprova
Nessun prodotto supportato trovato
Nessun prodotto
- Serviamo alcuni clienti
- Iniziare
Richiedi assistenza
Connetti al tuo lettore
Foto rimossa
@@ -110,8 +167,8 @@ Language: it
Nome, riepilogo e descrizione
Puoi modificare o rigenerare i dettagli del prodotto prima di salvarli.
Programmi
- Campagne Google
Nessun programma in questo periodo
+ Campagne Google
Connettiti ora
Carrello
Genera dettagli del prodotto
@@ -122,24 +179,23 @@ Language: it
Lascia generare a noi i dettagli del prodotto
Ricevi pagamenti con carta
Totale
- Imposte
Subtotale
Pagamento avvenuto correttamente
Pagamento non riuscito. Riprova.
Icona della carta
Prodotti
- %d elemento
- Cancella
Aumenta le vendite e genera più traffico con Google Ads
Google per WooCommerce
Nessuna regola sulla quantità
+ %d elemento
+ Cancella
+ Imposte
Pubblico
Annulla
- Esci
- Desideri uscire da POS?
Esci da POS
- Rimuovi %s dal carrello
Pagamento
+ Rimuovi %s dal carrello
+ Esci
Stato del lettore sconosciuto
Pagamento
Lettore connesso
@@ -170,13 +226,13 @@ Language: it
Hai ancora bisogno di aiuto? Contattaci
Impossibile caricare il report sull\'utilizzo dei coupon
Nessun coupon utilizzato in questo periodo
- Visualizza tutti i codici promozionali
Usi
- Codici promozionali
Magazzino
Visualizza tutti i messaggi
Impossibile caricare i prodotti più venduti
N/D
+ Visualizza tutti i codici promozionali
+ Codici promozionali
Resto dovuto
Contanti ricevuti
Codici promozionali più attivi
@@ -208,8 +264,8 @@ Language: it
Nascondi %s
Completata
Feedback
- Assicurati di disporre della versione più recente di WooCommerce sul tuo sito e di aver attivato Analisi WooCommerce.
Non possiamo visualizzare il tuo\n analisi del negozio
+ Assicurati di disporre della versione più recente di WooCommerce sul tuo sito e di aver attivato Analisi WooCommerce.
Visualizza tutte le attività
L\'analisi delle sessioni si basa su un numero unico di visitatori non disponibile per intervalli di date personalizzati
Dati della sessione non disponibili
@@ -222,11 +278,11 @@ Language: it
Annulla
Esci comunque
Sembra che tu non abbia ancora approvato la connessione dell\'app. Desideri uscire?
- Seleziona un\'immagine con una dimensione minima di 400x400 pixel
Immagine non valida
Sembra che il nome utente o la password inseriti non corrispondano. Controlla di nuovo le credenziali e riprova.
Se i dati non vengono ancora caricati, contatta il nostro team di supporto.
Nessun problema di connessione
+ Seleziona un\'immagine con una dimensione minima di 400x400 pixel
Torna alla schermata precedente
Riprova la connessione
Connessione al tuo sito
@@ -257,9 +313,9 @@ Language: it
Pacchetti venduti
Campagne Blaze
Prodotti più venduti
- Vuoi davvero eliminare le modifiche apportate a questo prodotto?
Stai per annullare le modifiche a %s
La funzione statistiche non supporta la visualizzazione dei dati sui visitatori e sulle conversioni per intervalli di date arbitrari.\n\nTuttavia, puoi toccare un valore sul grafico per visualizzare i visitatori e le conversioni per quell\'intervallo specifico.
+ Vuoi davvero eliminare le modifiche apportate a questo prodotto?
Dati sui visitatori e sulle conversioni non disponibili
Gestisci il tuo abbonamento
Abbonamenti
@@ -280,7 +336,6 @@ Language: it
Suggerimenti
Inserisci un dominio
Scegli un dominio
- Visualizza tutte le analisi del negozio
Annuale
Mensile
Settimanale
@@ -290,6 +345,7 @@ Language: it
Collega un altro negozio
Creare un altro negozio?
Nome del negozio
+ Visualizza tutte le analisi del negozio
Attendi…
Aggiornamento dello stato delle scorte
Si è verificato un problema. Riprova.
@@ -320,17 +376,17 @@ Language: it
Errore nel cestinare l\'ordine
Ordine cestinato
Sembra che ci sia un problema con il tuo sito.\n\nContatta il tuo provider di hosting per ulteriore assistenza.
+ Sembra non ci sia connessione a Internet.\n\nAssicurati che il Wi-Fi sia acceso. Se stai utilizzando i dati mobili, accertati che siano abilitati nelle impostazioni del dispositivo.
Sembra esserci un problema con la connessione Jetpack.\n\nMa non preoccuparti, il nostro team di supporto è qui per aiutarti. Contattaci e saremo lieti di assisterti.
Sembra che non sia possibile lavorare correttamente con la risposta del sito.\n\nMa non preoccuparti, il nostro team di supporto è qui per aiutarti. Contattaci e saremo lieti di assisterti.
Sembra che il tuo sito stia impiegando troppo tempo per rispondere.\n\nContatta il tuo provider di hosting per ulteriore assistenza.
- Sembra non ci sia connessione a Internet.\n\nAssicurati che il Wi-Fi sia acceso. Se stai utilizzando i dati mobili, accertati che siano abilitati nelle impostazioni del dispositivo.
- Prodotto non selezionato
Continua a leggere
Contatta il supporto
- Recupero degli ordini del sito
- Connessione ai server di WordPress.com
Connessione Internet
+ Prodotto non selezionato
Aggiungi statistiche sugli intervalli di date personalizzate
+ Recupero degli ordini del sito
+ Connessione ai server di WordPress.com
Non è stata trovata alcuna posizione.\nRiprova.
Visualizzazioni della pagina delle sessioni
Tipo di dispositivo
@@ -396,46 +452,42 @@ Language: it
URL del prodotto
Parametri URL
URL di destinazione
- Inserisci manualmente
- Ricerca non riuscita.\nRiprova
Inizia a digitare il Paese, lo Stato o la città per visualizzare le opzioni disponibili
Facendo clic su \"Invia campagna\" accetti i <a href=\'termsOfService\'><u>Termini di servizio</u></a> e la <a href=\'advertisingPolicy\'><u>Politica sulla pubblicità</u></a> e autorizzi gli addebiti tramite il tuo metodo di pagamento per il budget e la durata scelti. <a href=\'learnMore\'><u>Scopri di più</u></a> sui budget e sui pagamenti per il lavoro legato ad Articoli promossi.
+ Inserisci manualmente
+ Ricerca non riuscita.\nRiprova
Invia campagna
- Il caricamento dei metodi di pagamento non è riuscito, riprova cliccando qui!
Aggiungi un metodo di pagamento
- Caricamento dei metodi di pagamento
Totale
Campagna Blaze
Totali pagamenti
Pagamento
Cerca posizioni
+ Il caricamento dei metodi di pagamento non è riuscito, riprova cliccando qui!
+ Caricamento dei metodi di pagamento
Impossibile salvare la ricevuta
Impossibile scaricare la ricevuta
Impossibile individuare un\'applicazione con cui la ricevuta possa essere condivisa
Purtroppo non siamo riusciti a caricare una ricevuta per questo ordine
- Suggerito dall\'IA
%d caratteri rimanenti
Descrizione
Slogan
Cambia immagine
Applica
- Inizia
%1$s giorni
- Imposta durata
Le impressioni riflettono la frequenza con cui il tuo annuncio appare ai potenziali clienti.\n\n\n Anche se i numeri esatti non possono essere garantiti a causa delle fluttuazioni del traffico online e del comportamento degli utenti, il nostro obiettivo è far avvicinare il più possibile le impressioni effettive del tuo annuncio al numero di destinatari.\n\n\n Ricorda, le impressioni riguardano la visibilità, non l\'azione intrapresa dai visitatori.
Fatto
Impressioni
Aggiorna
Modifica
- Durata
Persone stimate raggiunte al giorno
%1$s giornaliero
per %1$s giorni
- Spesa totale
- Quanto vorresti spendere per la tua campagna promozionale?
Imposta il tuo budget
Tutti
+ Suggerito dall\'IA
%1$s giorni da %2$s
+ Data iniziale
Non mostrare più
Ricordamelo più tardi
Hai un minuto? Aiutaci a migliorare le nostre funzionalità assistite dall\'intelligenza artificiale con un rapido feedback.
@@ -448,27 +500,26 @@ Language: it
Budget
Dettagli
Acquista ora
- Modifica annuncio
Anteprima
+ Modifica annuncio
Disabilitato
Selezione prodotto
Seleziona prodotto %s
<b>Pubblica</b>: guarda come inizia la tua promozione e tieni traccia del suo successo.
- <b>Breve riassunto:</b> invia il tuo annuncio per un controllo veloce da parte del moderatore.
<b>Imposta il tuo budget:</b> decidi i costi e la durata della tua campagna.
<b>Personalizza il targeting:</b> seleziona il pubblico in base alla posizione o agli interessi e visualizza la portata potenziale.
<b>Scegli un prodotto:</b> scegli cosa promuovere con Blaze.
+ <b>Breve riassunto:</b> invia il tuo annuncio per un controllo veloce da parte del moderatore.
Gestisci magazzino
Magazzino non gestito
- Scopri come funziona Blaze
Avvia la tua campagna
- I tuoi annunci su milioni di siti web nelle reti WordPress.com e Tumblr.
Raggiungi un ampio pubblico
- \"Il nostro strumento presenta il tuo prodotto dove gli acquirenti interessati possono trovarlo\".
Raggiungere tutto il mondo non è mai stato così facile
+ I tuoi annunci su milioni di siti web nelle reti WordPress.com e Tumblr.
+ \"Il nostro strumento presenta il tuo prodotto dove gli acquirenti interessati possono trovarlo\".
+ Scopri come funziona Blaze
Lancia annunci in pochi minuti, senza bisogno di esperienza o di grandi budget, a partire da soli $ 5 al giorno.
Avvio rapido, grande impatto
- Il nostro strumento è stato progettato per consentire ai venditori di impostare annunci semplici e veloci per incrementare al massimo il traffico.
Promuovi
Tutto pronto per promuovere
Mostra i tuoi prodotti a milioni di persone
@@ -479,6 +530,7 @@ Language: it
Il codice dovrebbe essere nel formato XXXX-XXXX-XXXX-XXXX
Inserisci codice
Codice promozionale
+ Il nostro strumento è stato progettato per consentire ai venditori di impostare annunci semplici e veloci per incrementare al massimo il traffico.
Impossibile caricare i temi.
Configurazione completata
Aggiornamento quantità annullato
@@ -550,45 +602,45 @@ Language: it
Messaggio di ringraziamento
Nota: per attivare questa impostazione, l\'abbonamento non deve avere un periodo di prova o una data di rinnovo sincronizzata.
Attiva questa opzione per addebitare la spedizione solo una volta sull\'ordine iniziale.
- Attivata
Spedizione singola
Documenti e altri file sul dispositivo
+ Attivata
✨Crea messaggio di ringraziamento
Addebita imposte
I fondi disponibili vengono depositati automaticamente, ogni %s.
I fondi disponibili vengono depositati automaticamente, ogni giorno.
I fondi saranno disponibili dopo un periodo di attesa di %d giorni.
- Seleziona una variante
- Scegli variante
\" %1$s \" -> %2$s
- seleziona una variante
%1$s elementi selezionati
%1$s elemento selezionato
Seleziona %1$s
+ Seleziona una variante
+ seleziona una variante
più di %1$s elementi
più di %1$s elemento
meno di %1$s elementi
tra %1$s e %2$s elementi
%d elementi
%d elemento
+ Scegli variante
Cambia la quantità del prodotto da %1$.2f a %2$.2f
- Salva configurazione
Configurazione
Prodotto %s
Configura
+ Prodotto in abbonamento variabile
+ Prodotto in abbonamento semplice
+ Salva configurazione
Facoltativamente, la commissione di iscrizione verrà addebitata immediatamente, anche se il prodotto ha una prova gratuita o le date di pagamento sono sincronizzate.
Abbonamento a un prodotto con varianti
- Prodotto in abbonamento variabile
Un unico abbonamento al prodotto che attiva pagamenti ricorrenti
- Prodotto in abbonamento semplice
Un periodo di attesa opzionale prima dell\'addebito del primo pagamento ricorrente. Eventuali commissioni di iscrizione saranno comunque addebitate all\'inizio dell\'abbonamento. Il periodo di prova non può superare: 90 giorni, 52 settimane, 24 mesi o 5 anni.
Periodo di prova dell\'abbonamento
Scadenza dell\'abbonamento
+ PRODOTTO
IMPORTI PERSONALIZZATI
TOTALI PAGAMENTI
NOTE ORDINE
PRODOTTI
- PRODOTTO
CLIENTE
Fornisci una chiave di sicurezza per continuare.
Si è verificato un errore con l\'accesso alla chiave di sicurezza
@@ -605,12 +657,12 @@ Language: it
Stimato
Comprimi/espandi il riepilogo dell\'acconto
Scopri di più su quando riceverai i fondi
- I fondi disponibili vengono depositati automaticamente, ogni mese il giorno %s.
- I fondi saranno disponibili dopo un periodo di attesa di %d giorno.
Fondi in sospeso
Fondi disponibili
- Imposte
Prodotti
+ Imposte
+ I fondi disponibili vengono depositati automaticamente, ogni mese il giorno %s.
+ I fondi saranno disponibili dopo un periodo di attesa di %d giorno.
Totali pagamenti
Indirizzo e-mail o nome utente
Impossibile creare un ordine con importo personalizzato
@@ -634,7 +686,6 @@ Language: it
Incrementa le vendite nel tuo negozio con Blaze
Si è verificato un errore durante l\'aggiornamento dell\'elenco delle campagne. Riprova più tardi.
Seleziona la fonte degli elementi multimediali
- Si è verificato un errore durante la generazione del nome e della descrizione del prodotto.
Nessun testo rilevato. Seleziona un\'altra foto del pacchetto o inserisci manualmente i dettagli del prodotto.
Aggiungi prodotto
Scansiona codice a barre
@@ -656,27 +707,24 @@ Language: it
Prova un pagamento di %s con la tua carta di debito o di credito:\nti verrà rimborsato quando hai terminato.
È facile, sicuro e privato.
Accetta tutti i tipi di pagamenti di persona direttamente\ndal tuo telefono. Non è necessario alcun hardware aggiuntivo.
- Budget
- Clic
- Impressioni
Rifiutata
Completata
Attiva
In moderazione
Crea la campagna
Aumenta la visibilità e vendi i tuoi prodotti rapidamente.
- Campagna Blaze
Il simbolo del contactless è un marchio registrato di proprietà e utilizzato su concessione di EMVCo, LLC.
5. Dopo aver visualizzato il segno di spunta \"Fatto\", il negozio elaborerà il pagamento e la transazione verrà completata.
+ Campagna Blaze
4. Il cliente posiziona la carta orizzontalmente sulla parte superiore del telefono, sopra il simbolo del contactless.
3. Sempre tenendo il tuo telefono in mano, rivolgilo verso il cliente.
2. Tocca \"Ricevi pagamenti\" e scegli \"Tocca per pagare\".
- 1. Crea un ordine
Come funziona
Scopri di più sui lettori di carte
Per accettare pagamenti superiori a questo limite, potresti acquistare un lettore di carte che accetta l\'inserimento del PIN.
Non supportiamo l\'inserimento del PIN con Tocca per pagare su Android.
%1$s - In questo Paese, alcune carte richiedono il PIN per le transazioni superiori a %2$s.
+ 1. Crea un ordine
Informazioni importanti
Tocca per pagare ti consente di accettare tutti i tipi di pagamento contactless: dalle carte di credito e di debito fisiche ai portafogli digitali. Tutto senza il bisogno di acquistare un lettore di carte fisico.
Che cos\'è Tocca per pagare?
@@ -691,46 +739,23 @@ Language: it
IMPOSTAZIONI
Aggiungi dettagli manualmente utilizzando l\'e-mail
Impossibile elaborare la richiesta di accesso all\'app
- Errore durante la copia del nome del prodotto negli appunti.
- Nome del prodotto copiato negli appunti.
- Nome del prodotto generato dall\'IA
Errore durante la copia della descrizione del prodotto negli appunti.
Descrizione del prodotto copiata negli appunti.
Generazione del prodotto non riuscita. Riprova
Cancella l\'indirizzo e non utilizzare più questa aliquota
Configura una nuova aliquota d\'imposta per questo ordine
Aggiunta automatica dell\'aliquota d\'imposta
- Si è verificato un problema durante la generazione del nome del prodotto. Riprova.
- Rigenera
- Scrivilo per me
- Parlaci del tuo prodotto e di cosa lo rende unico.
- Lascia che l\'IA generi titoli accattivanti per te.
- Nome del prodotto
Prova a disattivare il filtro delle recensioni non lette per visualizzarle tutte
Nessuna recensione del prodotto non letta
- Tono selezionato
Convincente
Note floreali
Formale
Casual
- Imposta il tone of voice per dare forma a una presentazione del prodotto che sia in linea con il tuo brand.
Tone of voice
Dettagli
- Descrizione del prodotto
Nome del prodotto
- Puoi sempre modificare i dettagli riportati di seguito in un secondo momento.
Anteprima
- Crea dettagli del prodotto
- Imposta il tone of voice
- Aggiungi funzionalità, vantaggi o dettagli chiave per consentire al tuo prodotto di essere trovato online.
Ad esempio tessuto morbido, cuciture resistenti e design unico
- Metti in evidenza ciò che rende unico il tuo prodotto e lascia che l\'IA faccia la magia.
- Informazioni sul tuo prodotto
- Suggerisci un nome
- Ad esempio tessuto morbido, cuciture resistenti e design unico
- Nome del prodotto
- Oppure amplia le tue scelte toccando per ottenere ulteriori suggerimenti sui nomi.
- Aggiungi nome del prodotto
Con tecnologia IA. <a href=\'guidelines\'><u>Scopri di più</u></a>.
Aggiungi un prodotto e i dettagli manualmente
Aggiungi manualmente
@@ -743,18 +768,18 @@ Language: it
Aggiungi questa aliquota a tutti gli ordini creati
Modifica le aliquote d\'imposta
Modifica le aliquote d\'imposta nella pagina dell\'amministratore
- Aggiungi le aliquote d\'imposta nella pagina dell\'amministratore. Qui verranno visualizzate solo le aliquote d\'imposta con le informazioni sulla posizione.
Impossibile trovare le aliquote d\'imposta
- Scopri altri fornitori di servizi di pagamento e \nscegline uno.
Metodi di pagamento
+ Aggiungi le aliquote d\'imposta nella pagina dell\'amministratore. Qui verranno visualizzate solo le aliquote d\'imposta con le informazioni sulla posizione.
+ Scopri altri fornitori di servizi di pagamento e \nscegline uno.
Immagini e video sul dispositivo
Risolvi ora
Completa la configurazione
Imposta l\'aliquota d\'imposta
- Attiva
Imposta la nuova aliquota d\'imposta
- WooPayments
+ Attiva
Configura
+ WooPayments
Modifica le aliquote d\'imposta nella pagina dell\'amministratore
Questa azione modificherà l\'indirizzo del cliente nella località dell\'aliquota d\'imposta selezionata.
Pulsante che apre la finestra di dialogo delle informazioni sulle aliquote d\'imposta
@@ -802,8 +827,8 @@ Language: it
Totale ordine
Percentuale calcolata
Importo calcolato
- Nome del negozio
La personalizzazione del nome del negozio può anche agevolarne l\'ottimizzazione per i motori di ricerca.
+ Nome del negozio
Dai un nome al tuo negozio
Abilita NFC
Pacchetti di fornitura di piccole quantità (marcature richieste)
@@ -831,18 +856,18 @@ Language: it
Classe 4 - Pacchetto (solidi infiammabili)
Classe 3 - Pacchetto (igienizzante per mani, alcol disinfettante, prodotti a base di etanolo, liquidi infiammabili, ecc.)
Classe 1 - Pacchetto di propellente per razzi/fusibile di sicurezza
- Pacchetto di etanolo conforme alle prescrizioni per il volo - (spedizioni di fragranze e di igienizzanti per mani autorizzati)
OK
+ Pacchetto di etanolo conforme alle prescrizioni per il volo - (spedizioni di fragranze e di igienizzanti per mani autorizzati)
I materiali potenzialmente pericolosi includono articoli come batterie, ghiaccio secco, liquidi infiammabili, aerosol, munizioni, fuochi d\'artificio, smalto per unghie, profumi, vernici, solventi e altro ancora. I materiali pericolosi devono essere spediti in pacchi separati.
Contiene materiali pericolosi
Inserisci il titolo del prodotto.
La piattaforma di eCommerce che cresce con te
Abbonamento variabile
- Rimuovi codice promozionale
Tutti amano un\'offerta
Non hai ancora creato nessun codice promozionale. Crea un codice promozionale per applicarlo a questo ordine.
Vai ai codici promozionali
Seleziona un codice promozionale
+ Rimuovi codice promozionale
Impossibile creare il codice promozionale
Codice promozionale creato
Crea
@@ -855,8 +880,6 @@ Language: it
Sconto fisso sul prodotto
Sconto fisso sul carrello
Sconto in percentuale
- Tipo di codice promozionale - Prodotto fisso
- Tipo di codice promozionale - Carrello fisso
Tipo di codice promozionale - Sconto percentuale
Crea codice promozionale
Aggiungi codice promozionale
@@ -870,6 +893,8 @@ Language: it
Esegui un ordine di prova per assicurarti che il processo WooCommerce offra un\'esperienza cliente fluida
Aggiungi dettagli manualmente
Cerca clienti per
+ Tipo di codice promozionale - Prodotto fisso
+ Tipo di codice promozionale - Carrello fisso
Altro motivo (specificare)
Faccio parte di una team e la decisione va presa insieme a tutti i membri.
Trovo che il prezzo del servizio sia un fattore significativo nella mia decisione.
@@ -878,12 +903,12 @@ Language: it
Aiutaci a capire le tue decisioni relative all\'abbonamento. Il tuo feedback è importante.
Nessun indirizzo e-mail
Nessun nome
- Cerca un cliente esistente o
Ultimo aggiornamento: %s (aggiornamenti ogni 30 minuti)
Ultimo aggiornamento: %s
- <a href=\'\'>Scopri di più</a> sull\'accettazione di pagamenti con Tocca per pagare su Android
+ Cerca un cliente esistente o
Ricevi pagamenti
Impossibile aggiungere prodotti senza un prezzo specificato
+ <a href=\'\'>Scopri di più</a> sull\'accettazione di pagamenti con Tocca per pagare su Android
Impossibile aggiungere i prodotti che non sono stati pubblicati
aggiungi il cliente
Vai su Impostazioni
@@ -949,19 +974,18 @@ Language: it
Impossibile generare il messaggio per la condivisione. Riprova.
Scopri di più sulla funzionalità dell\'IA
Aggiungi un messaggio opzionale
- Scrittura…
Scrivi con l\'IA
Promuovi i prodotti con Blaze
Blaze
Generatore di contenuti IA disponibile
Promuovi con Blaze
+ Scrittura…
Condividi prodotto
Complimenti. Il nuovo negozio è ora più vicino.
Primo prodotto creato 🎉
Il sistema ha terminato l\'app Woo mentre era in esecuzione in background. Puoi provare a usarla di nuovo.
Il sistema ha terminato l\'app Woo mentre era in esecuzione in background. Puoi provare a usarla di nuovo.
La scheda è stata rimossa troppo presto
- Prodotto con varianti
La nostra politica sui cookie spiega l\'uso che noi e altri facciamo dei cookie e come puoi gestirli.
Informativa sui cookie
Le tue informazioni ci aiutano a migliorare i nostri prodotti, il marketing e a personalizzare la tua esperienza su WooCommerce.
@@ -973,6 +997,7 @@ Language: it
Analytics
Gestisci la privacy
La privacy è di fondamentale importanza per noi e lo è sempre stata. Usiamo, archiviamo ed elaboriamo i dati personali per ottimizzare la nostra app (e la tua esperienza) in vari modi. Alcuni usi dei dati sono assolutamente necessari per un corretto funzionamento e altri puoi personalizzarli dalle Impostazioni.
+ Prodotto con varianti
Per aiutarci a migliorare le prestazioni dell\'app e correggere bug occasionali, attiva il rapporto di arresto anomalo.
Segnala arresti anomali
Rapporti
@@ -981,7 +1006,6 @@ Language: it
Privacy
Scopri di più sui dati che raccogliamo sul tuo negozio e sulle tue opzioni per il controllo di questa condivisione dei dati.
Tracciabilità dell\'utilizzo
- Ulteriori opzioni per la privacy sono disponibili per gli utenti di woocommerce.com. Controlla qui per saperne di più.
Opinioni del web
Ulteriori opzioni per la privacy
Si è verificato un errore durante l\'aggiornamento delle impostazioni sulla privacy
@@ -994,6 +1018,7 @@ Language: it
Non è possibile aggiungere direttamente il prodotto variabile. Seleziona una variante specifica
Scansione non riuscita. Riprova più tardi
Impossibile trovare il prodotto con SKU %s. Impossibile aggiungere all\'ordine
+ Ulteriori opzioni per la privacy sono disponibili per gli utenti di woocommerce.com. Controlla qui per saperne di più.
Scansione non riuscita. Riprova più tardi
Scansiona codice a barre
La spedizione in Paesi che seguono le regole doganali dell\'Unione europea (UE) ora richiede la descrizione chiara di ogni elemento. Ad esempio, se stai inviando capi di vestiario, devi indicare il tipo di abbigliamento (come camicie da uomo, gilet da ragazza, giacca da ragazzo) affinché la descrizione sia accettabile. In caso contrario, le spedizioni potrebbero subire dei ritardi o essere fermate alla dogana.
@@ -1017,16 +1042,16 @@ Language: it
Aggiungi i prodotti tramite scanner
Ignora
Scopri di più
- Quando spedisci in Paesi che seguono le norme doganali dell\'Unione europea (UE), devi fornire una descrizione chiara e specifica per ogni elemento. In caso contrario, le spedizioni potrebbero subire dei ritardi o essere fermate alla dogana.
Resta aggiornato e aumenta la sicurezza del negozio. Esplora Jetpack ora.
Ricevi notifiche sugli ordini e altro ancora
+ Quando spedisci in Paesi che seguono le norme doganali dell\'Unione europea (UE), devi fornire una descrizione chiara e specifica per ogni elemento. In caso contrario, le spedizioni potrebbero subire dei ritardi o essere fermate alla dogana.
Mostra o nascondi l\'elenco delle impostazioni del negozio
Elenco delle impostazioni del negozio
- Puoi ripristinarlo quando lo necessiti da Menu > Impostazioni > Negozio
Nascondi l\'elenco delle impostazioni del negozio
Nascondi l\'elenco delle impostazioni del negozio
Visualizza ordine
Il pagamento di Tocca per pagare di prova è stato rimborsato correttamente
+ Puoi ripristinarlo quando lo necessiti da Menu > Impostazioni > Negozio
Rimborso non riuscito. Prova a eseguire manualmente il rimborso
Rimborso del pagamento di prova in corso…
Continuando, accetti i nostri <a href=\'termsOfService\'><u>Termini di servizio.</u></a>
@@ -1054,7 +1079,6 @@ Language: it
Incrementa le vendite con offerte speciali
Visualizza il tuo negozio
Rimani aggiornato
- Unisciti ai pagamenti mobili
Gestisci di più in Admin
Generale
Impostazioni
@@ -1114,14 +1138,14 @@ Language: it
Abbonamento
Il lettore di carte accetta pagamenti contactless, chip e tramite strisciata con carte di debito e di credito.
Accetta in modo sicuro pagamenti contactless direttamente dal tuo telefono.
- Usa il telefono per accettare i pagamenti\ncon carta. Prova ora.
- Condividi il feedback
Impossibile accedere perché la creazione della password dell\'applicazione non è stata approvata.
Sito in caricamento…
+ Condividi il feedback
+ Usa il telefono per accettare i pagamenti\ncon carta. Prova ora.
+ Caricamento in corso…
Si è verificato un errore durante il recupero del sito web
Riprova con la pagina Bacheca
Accedi
- Caricamento in corso…
Terminato il %s
Il tuo abbonamento è scaduto e hai un accesso limitato a tutte le funzionalità.
%1$d giorni
@@ -1136,13 +1160,11 @@ Language: it
Errore durante il recupero dei dettagli del piano.
Ora sei un abbonato %1$s. Hai accesso a tutte le nostre funzionalità fino al giorno %2$s.
La tua prova gratuita è terminata e hai accesso limitato a tutte le funzionalità. Abbonati a %1$s ora.
- Stai utilizzando il periodo di prova di %1$d giorni. La prova gratuita terminerà il %2$s. Esegui l\'upgrade per sbloccare nuove funzionalità e mantenere attivo il tuo negozio.
Stato dell\'abbonamento
Risoluzione dei problemi
Attuale: %s
Segnala il problema con l\'abbonamento
Aggiorna ora
- %1$s rimasti nella tua prova.
Prova terminata
La tua prova è terminata.
Ops, si sono verificati alcuni errori imprevisti.
@@ -1158,6 +1180,8 @@ Language: it
Pubblica il mio negozio
Per lanciare il tuo negozio, hai bisogno di eseguire l\'aggiornamento al nostro piano. <u>Aggiorna</u>
Cerca domini
+ Stai utilizzando il periodo di prova di %1$d giorni. La prova gratuita terminerà il %2$s. Esegui l\'upgrade per sbloccare nuove funzionalità e mantenere attivo il tuo negozio.
+ %1$s rimasti nella tua prova.
Accesso non riuscito con codice dello stato %1$s
Impossibile accedere perché non possiamo identificare l\'URL di amministrazione del negozio
Impossibile accedere perché non possiamo identificare l\'URL di accesso del negozio
@@ -1205,31 +1229,31 @@ Language: it
Recupero dello stato di Jetpack
Si è verificato un problema. Riprova più tardi.
Prova un pagamento
+ Registrazione del nome di dominio in corso…
+ Seleziona il Paese
+ Seleziona uno stato
Ricevi pagamenti con carta\ncon il tuo telefono
Tocca per pagare
AZIONI
Si è verificato un errore durante la registrazione del dominio
- Seleziona uno stato
- Seleziona il Paese
- Registrazione del nome di dominio in corso…
- Registra dominio
- CAP
- Stato (non disponibile)
- Stato
- Città
- Indirizzo 2
- Indirizzo
- Paese
- Prefisso internazionale
Telefono
- Organizzazione (opzionale)
+ Prefisso internazionale
+ Paese
+ Indirizzo
+ Indirizzo 2
+ Città
+ Stato
+ Stato (non disponibile)
+ CAP
+ Registra dominio
Per agevolarti, abbiamo precompilato le tue informazioni di contatto\n di WordPress.com. Rileggi per verificare che le informazioni che desideri utilizzare per questo dominio siano corrette.
- Informazioni di contatto per il dominio
- Registra pubblicamente
- Registra privatamente con protezione della privacy
- Per %s, inserisci un valore valido
- Registrando questo dominio accetti i nostri %1$sterme e le nostre condizioni%2$s
+ Organizzazione (opzionale)
I proprietari del dominio devono condividere le informazioni di contatto in un database pubblico di tutti i domini.\n Con la Protezione della privacy, pubblichiamo le nostre informazioni al posto delle tue e ti inoltriamo privatamente qualsiasi comunicazione.
+ Registrando questo dominio accetti i nostri %1$sterme e le nostre condizioni%2$s
+ Per %s, inserisci un valore valido
+ Registra privatamente con protezione della privacy
+ Registra pubblicamente
+ Informazioni di contatto per il dominio
Protezione della privacy
Solo gli amministratori del negozio possono accedere alle impostazioni del dominio
In alternativa, continua a usare il Link
@@ -1237,10 +1261,10 @@ Language: it
Inserisci la password del tuo account WordPress.com per connettere Jetpack
Accedi con il tuo account WordPress.com per installare Jetpack
Accedi con il tuo account WordPress.com per connettere Jetpack
- Puoi trovare le impostazioni del dominio in Impostazioni -> Domini
L\'indirizzo del tuo sito è in fase di configurazione. Potrebbero essere necessari fino a 30 minuti prima che il tuo dominio inizi a funzionare.
Congratulazioni per il tuo acquisto
Gratis per il primo anno
+ Puoi trovare le impostazioni del dominio in Impostazioni -> Domini
Vuoi davvero uscire dal tuo account?
Impossibile caricare i domini del sito
%1$d/%2$d completato
@@ -1288,19 +1312,19 @@ Language: it
L\'operazione non richiederà molto tempo
Preparazione del lettore integrato…
Il lettore integrato è pronto
- Lettore di carte
Tocca per pagare
Tasso di conversione
Sessioni
Nessuna sessione in questo periodo
Confrontato con
Dominio
+ Lettore di carte
Dove si trovano le Password applicazione?
Sembra che la funzione Password applicazione sia disabilitata nel tuo sito %1$s.\n Abilitalo all\'uso dell\'app WooCommerce.
Apri la pagina di installazione
- Si è verificato un errore durante l\'invio della risposta
- Risposta inviata!
Rispondi
+ Risposta inviata!
+ Si è verificato un errore durante l\'invio della risposta
Seleziona tutto
Aggiorna prezzo
Aggiorna stato
@@ -1311,17 +1335,17 @@ Language: it
Sono già generate tutte le varianti.
Nessuna variante generata
Seleziona più elementi
- Nessun dominio disponibile per questa ricerca
Generazione delle varianti
Questo creerà una nuova variazione per ogni possibile combinazione degli attributi di variazione (%1$d varianti).
Generare tutte le varianti?
Attualmente la creazione è supportata per un massimo di %1$d varianti. La generazione delle varianti per il prodotto creerà %2$d varianti.
Limite di generazione superato
Crea le variant per tutte le combinazioni degli attributi.
- Genera tutte le varianti
Crea una nuova variante. Impostare manualmente quali attributi appartengono al prodotto variabile.
Aggiungi nuova variante
Aggiungi variazione
+ Genera tutte le varianti
+ Nessun dominio disponibile per questa ricerca
Esci senza connessione
Continua connessione
Prova a connetterti di nuovo per accedere al tuo negozio.
@@ -1333,7 +1357,6 @@ Language: it
Prova di nuovo l\'attivazione
Prova di nuovo l\'installazione
Ottieni assistenza
- Riprova e contatta il supporto se l\'errore persiste.
Si è verificato un errore durante la comunicazione con il sito.
Non disponi dell\'autorizzazione per gestire i plugin in questo negozio
Errore di autorizzazione della connessione a Jetpack
@@ -1357,6 +1380,7 @@ Language: it
Installazione di Jetpack
Accedi a <b>%1$s</b> con le credenziali del tuo negozio per connetterti a Jetpack.
Accedi a <b>%1$s</b> con le credenziali del tuo negozio per installare Jetpack.
+ Riprova e contatta il supporto se l\'errore persiste.
Prepara le credenziali del negozio.
Connetti il negozio a Jetpack per permetterne l\'accesso su questa app.
Installa il plugin Jetpack gratuito per accedere al negozio su questa app.
@@ -1369,8 +1393,8 @@ Language: it
Aggiorna il lettore di carte simulato
Collega Jetpack
Collega negozio
- Ecco dove gli utenti ti troveranno su Internet. Non preoccuparti. Puoi cambiarlo più tardi.
Visitatori
+ Ecco dove gli utenti ti troveranno su Internet. Non preoccuparti. Puoi cambiarlo più tardi.
In alternativa, accedi con la password
Il lettore di carte simulato è stato disattivato
Chiave del lettore simulato
@@ -1407,12 +1431,12 @@ Language: it
Prova con un altro indirizzo
Intervallo di date personalizzato
Personalizza
- Cos\'è WordPress.com?
Creazione di un nuovo account
Scegli una password
Il tuo indirizzo e-mail
Crea il tuo sito \nin pochi minuti
Toccando il pulsante Configura Jetpack, accetti i nostri <a href=\'terms\'>Termini di servizio</a> e la <a href=\'sync\'>condivisione delle informazioni</a> con WordPress.com.
+ Cos\'è WordPress.com?
Attiva il lettore di carte simulato
Contatta il proprietario del sito per un invito al sito come gestore del negozio o amministratore per utilizzare l\'app.
Connessione a un sito WordPress.com
@@ -1433,8 +1457,8 @@ Language: it
Analisi del negozio non disponibile! Esegui l\'aggiornament all\'ultima versione di WooCommerce per visualizzare le analisi del tuo negozio.
La rete non è disponibile.\nControlla la connessione dati o WiFi.
Accedi all\'app WooCommerce
- Recupero dei dati della connessione non riuscito…
Verifica della connessione a Jetpack…
+ Recupero dei dati della connessione non riuscito…
Impossibile verificare la connessione a Jetpack. Riprova.
Il sito %1$s dispone attualmente di un piano WordPress.com che non supporta l\'installazione di plugin. Aggiorna il tuo piano per utilizzare WooCommerce.
Sembra che l\'account non sia connesso a Jetpack di %1$s
@@ -1452,44 +1476,43 @@ Language: it
Prima volta in WooCommerce
Si è verificato un errore, contatta il supporto
Inserisci un indirizzo del sito
- Richiedi un link di accesso via e-mail
Non ricordi la password?
+ Richiedi un link di accesso via e-mail
Abbiamo notato che non hai ancora completato la configurazione dei Pagamenti di persona. <a href=\'\'>Continua con la configurazione</a>
- Pagamenti
- OK!
- Ora puoi accedere rapidamente e con facilità ai Pagamenti di persona e ad altre funzionalità
+ WC Admin
+ Accesso con l\'indirizzo del negozio
+ Altri siti
Pagamenti dalla scheda Menu
+ Ora puoi accedere rapidamente e con facilità ai Pagamenti di persona e ad altre funzionalità
+ OK!
+ Pagamenti
L\'e-mail non è utilizzata con un account WordPress.com.
- Altri siti
- Accesso con l\'indirizzo del negozio
- WC Admin
- Abbiamo appena inviato un link magico a
- Controlla la tua e-mail su questo dispositivo!
- Usa la password per accedere
- Accedi con il link magico
- Ti abbiamo appena inviato un link magico al tuo indirizzo e-mail. Tocca il link nell\'e-mail per accedere.
Accedi con le credenziali del sito
- Offri ai tuoi clienti consigli utili e pertinenti sui prodotti sommando upsell e cross-sell
- Aumenta le tue vendite con i prodotti collegati
- Inizia a vendere di persona in meno di 20 minuti con il nostro lettore di carte.
- Errore durante l\'aggiornamento dell\'ordine #%1$d
- Ordine #%1$d contrassegnato come completo
- Contrassegna\ncompletato
- Installa WooCommerce
- %1$s non è un sito WooCommerce.
- Passa a un negozio all\'altro
- Gestisci i miei ordini
- Crea o aggiorna i miei prodotti
- Controlla le mie analisi
- Tentativo di configurazione di un negozio
- Sto solo dando un\'occhiata
- Cosa ti porta in WooCommerce?
- Suggerimento
+ Ti abbiamo appena inviato un link magico al tuo indirizzo e-mail. Tocca il link nell\'e-mail per accedere.
+ Accedi con il link magico
+ Usa la password per accedere
+ Controlla la tua e-mail su questo dispositivo!
+ Abbiamo appena inviato un link magico a
Configura ora
+ Suggerimento
+ Cosa ti porta in WooCommerce?
+ Sto solo dando un\'occhiata
+ Tentativo di configurazione di un negozio
+ Controlla le mie analisi
+ Crea o aggiorna i miei prodotti
+ Gestisci i miei ordini
+ Passa a un negozio all\'altro
+ %1$s non è un sito WooCommerce.
+ Installa WooCommerce
+ Contrassegna\ncompletato
+ Ordine #%1$d contrassegnato come completo
+ Errore durante l\'aggiornamento dell\'ordine #%1$d
+ Inizia a vendere di persona in meno di 20 minuti con il nostro lettore di carte.
+ Aumenta le tue vendite con i prodotti collegati
+ Offri ai tuoi clienti consigli utili e pertinenti sui prodotti sommando upsell e cross-sell
Iniziamo!
Accedi con WordPress.com
Contatta il supporto
- Accedi con il tuo account WordPress.com
Ricevi aiuto!
Problemi con l\'accesso?
COD
@@ -1511,6 +1534,7 @@ Language: it
Puoi gestirli rapidamente e facilmente
Sappiamo che è essenziale per la tua attività
Prima volta in WooCommerce
+ Accedi con il tuo account WordPress.com
Nuovo ordine pari a $ 50 nel tuo negozio WooCommerce
Hai un nuovo ordine. 🎉
i dettagli
@@ -1519,10 +1543,10 @@ Language: it
Condividi rapporto dello stato del sistema
Copia il rapporto dello stato del sistema negli appunti
Continua a cercare
- Pagamento di persona per l\'ordine #%1$s per %2$s blog_id %3$s.
Cambia fornitore dei pagamenti
Rimborsato: %1$s
In attesa di pagamento
+ Pagamento di persona per l\'ordine #%1$s per %2$s blog_id %3$s.
Procedere con l\'installazione
Cosa da sapere prima dell\'installazione
Installa estensione
@@ -1544,8 +1568,8 @@ Language: it
bloccato
Per modificare Dettagli di pagamento o i Prodotti, modifica lo stato in Pagamento in sospeso.
Le parti di questo ordine non sono al momento modificabili
- Cerca clienti
Nessun cliente trovato
+ Cerca clienti
Non ora
Aggiungi estensione al negozio
Cos\'è WooCommerce Shipping?
@@ -1580,8 +1604,6 @@ Language: it
I prezzi attuali sono misti
Il prezzo attuale è %s
Il prezzo verrà aggiornato per %d varianti
- Misto
- Nessuno
Prezzo di vendita
Prezzo standard
Prezzo
@@ -1589,6 +1611,8 @@ Language: it
Aggiornamento in blocco
OK
Aggiornamento in blocco…
+ Misto
+ Nessuno
Recupero delle varianti in corso…
Ricerca delle categorie di prodotti non riuscita
Caricamento delle categorie di prodotti non riuscito
@@ -1603,11 +1627,10 @@ Language: it
Ottieni WooCommerce Shipping
Stampa etichette dal tuo telefono con WooCommerce Shipping.
Hai bisogno di un\'etichetta di spedizione?
- Cambia la quantità di prodotto da %1$d a %2$d
Aggiorna al prezzo standard
Aggiorna prezzo di vendita
+ Cambia la quantità di prodotto da %1$d a %2$d
Non supportiamo l\'estensione WooCommerce Stripe in %1$s
- Filtro
Cancella la selezione
Seleziona %d prodotto
Seleziona %d prodotti
@@ -1619,6 +1642,7 @@ Language: it
Attiva questa opzione se il codice promozionale non deve essere applicato agli articoli in offerta. I codici promozionali per prodotto funzionano solo se l\'articolo non è in offerta. I codici promozionali per carrello funzionano solo se nel carrello sono presenti articoli non in offerta.
Escludi elementi in offerta
Attiva questa opzione se il codice promozionale non può essere usato insieme ad altri.
+ Filtro
Solo utilizzo individuale
Limite di utilizzo per utente
Limite di utilizzo per X prodotti
@@ -1693,12 +1717,12 @@ Language: it
Prova altri mezzi per eseguire il rimborso
Il rimborso è stato rifiutato per una ragione sconosciuta
Siamo spiacenti, questo rimborso non può essere elaborato
+ Copia
Rimborso avvenuto correttamente
Elaborazione del rimborso
Rimborsa il pagamento
Rimborso non riuscito
Preparazione dell\'esecuzione del rimborso del pagamento
- Copia
Cerca codici promozionali
Impossibile generare il messaggio per la condivisione del codice promozionale
Errore durante la condivisione del codice promozionale.
@@ -1723,18 +1747,9 @@ Language: it
Pagamento: %s
Condividi il link di pagamento
Importo
- Importo
- Ordini scontati
- Prestazioni
- Spesa massima di %s
- Spesa minima di %s
- Riepilogo codice promozionale
Visualizza riepilogo codice promozionale
- Abbiamo lavorato per rendere possibile la visualizzazione e la modifica dei coupon sul tuo dispositivo!
Visualizza e modifica i codici promozionali
Non è stato trovato alcun codice promozionale
- %1$s senza %2$s
- %1$s e %2$s
tutto
Scaduto
Attivo
@@ -1752,6 +1767,15 @@ Language: it
\u2022 una recensione approvata
\u2022 %drecensioni approvate
%1$s (%2$s%%)
+ Importo
+ Ordini scontati
+ Prestazioni
+ Spesa massima di %s
+ Spesa minima di %s
+ Riepilogo codice promozionale
+ Abbiamo lavorato per rendere possibile la visualizzazione e la modifica dei coupon sul tuo dispositivo!
+ %1$s senza %2$s
+ %1$s e %2$s
Abbiamo lavorato per permetterti di creare ordini dal tuo dispositivo. Puoi provare questa funzionalità toccando il pulsante \"+\"
Torna presto per ulteriori suggerimenti e per la panoramica sulla crescita del tuo negozio
Congratulazioni, hai letto tutto.
@@ -1766,20 +1790,20 @@ Language: it
Il servizio XML-RPC è disattivato su questo sito.
Utilizza un\'e-mail non automatica per inviare un ticket di supporto
Non supportiamo gli account Stripe registrati in %1$s
- Non supportiamo l\'estensione WooCommerce Payments in %1$s
Premi il pulsante di accensione del tuo lettore
Una ricevuta è stata inviata a <strong>%s</strong>
Percentuale (%)
+ Non supportiamo l\'estensione WooCommerce Payments in %1$s
Rimuovi la commissione dall\'ordine
Rimuovi la spedizione dall\'ordine
Spedizione
Aggiungi spedizione
Aggiungi spedizione
Nome
- Importo
Tariffe
Dettagli cliente
Aggiungi l\'imposta
+ Importo
Modifica una nota del cliente
Modifica dettagli del cliente
Modifica lo stato dell\'ordine
@@ -1798,10 +1822,10 @@ Language: it
WooCommerce Payments
Gateway stripe WooCommerce
I pagamenti di persona funzioneranno solo con uno dei seguenti plugin attivato. Per continuare, contatta un amministratore del sito per disattivare uno di questi plugin:
- I pagamenti di persona funzioneranno solo con uno dei seguenti plugin attivato. Per continuare disattiva uno di questi plugin:
Conflitto con i plugin di pagamento rilevato
- Totale imposte
oppure
+ I pagamenti di persona funzioneranno solo con uno dei seguenti plugin attivato. Per continuare disattiva uno di questi plugin:
+ Totale imposte
Installa Jetpack
Pagamenti di persona attualmente non disponibile
Ordine creato
@@ -1872,9 +1896,7 @@ Language: it
Aggiungi un indirizzo di spedizione differente
In magazzino
%s in magazzino
- Aggiungi prodotti
Prodotti
- Aggiungi dettagli del cliente
Cliente
Contrassegna come Pagato
Questa azione creerà il tuo ordine e lo contrassegnerà come Pagato se hai ricevuto il pagamento al di fuori di WooCommerce
@@ -1882,6 +1904,8 @@ Language: it
Scegli il tuo metodo di pagamento
Le imposte sono calcolate automaticamente in base all\'indirizzo del negozio.
Imposta (%s%%)
+ Aggiungi dettagli del cliente
+ Aggiungi prodotti
Ricevi il pagamento%s
Modifica le imposte
Importo personalizzato
@@ -1934,19 +1958,19 @@ Language: it
Filtra stato
Stato
Data di fine
- Data di inizio
Seleziona le date
Intervallo personalizzato
- Crea un ordine con informazioni minime
Pagamento facile
- Crea un nuovo ordine manuale
Crea ordine
Crea ordine
Inserisci quantità
Ricevi il pagamento
Pagamento facile
- Crea ordini dal tuo dispositivo!
Dati analizzati
+ Data di inizio
+ Crea un ordine con informazioni minime
+ Crea un nuovo ordine manuale
+ Crea ordini dal tuo dispositivo!
Fatto
Connessione del tuo negozio
Attivazione
@@ -2030,8 +2054,8 @@ Language: it
Rapporto dello stato del sistema
Congratulazioni, ora puoi accettare pagamenti con carte di debito e di credito con WooCommerce Payments.
Ricevi i pagamenti con un lettore di carte
- L\'importo deve essere di almeno %1$s
OK
+ L\'importo deve essere di almeno %1$s
Nuova immagine dell\'icona della funzionalità
Cambia negozio
Aggiornamento del prodotto %1$s non riuscito
@@ -2072,10 +2096,10 @@ Language: it
Impossibile verificare automaticamente l\'indirizzo di spedizione: %s
Impossibile verificare automaticamente l\'indirizzo di origine. Visualizza su Google Maps per assicurarti che l\'indirizzo sia corretto.
Stiamo lavorando per rendere più semplice per te visualizzare i componenti aggiuntivi del prodotto dal dispositivo. Per ora, sarai in grado di visualizzare i componenti aggiuntivi per gli ordini. Puoi creare e modificare questi componenti aggiuntivi nella bacheca web.
+ Salva
Visualizza i componenti aggiuntivi dal dispositivo.
Se rinomini un componente aggiuntivo nella tua bacheca web, tieni presente che gli ordini precedenti non mostreranno più quel componente aggiuntivo all\'interno dell\'app.
Visualizza componenti aggiuntivi
- Salva
Dettagli del caricamento (%d)
Non è stato possibile caricare %d file
Non è stato possibile caricare %d file
@@ -2107,16 +2131,17 @@ Language: it
Pagamenti di persona non è disponibile nella Modalità di prova. Disattivare la funzione per proseguire.
Pagamenti di persona attualmente non disponibile
Ci sono requisiti in sospeso sull\'account. Completa i requisiti tramite %1$s per continuare ad accettare Pagamenti di persona.
- Il tuo account presenta requisiti in sospeso
C\'è almeno un requisito scaduto sul tuo account. Completalo per riprendere a utilizzare Pagamenti di persona
Pagamenti di persona attualmente non disponibile
Potrai accettare Pagamenti di persona non appena avremo terminato la revisione del tuo account.
+ Il tuo account presenta requisiti in sospeso
Pagamenti di persona attualmente non disponibile
Siamo spiacenti, ma non possiamo supportare Pagamenti di persona per questo negozio.
Ricarica dopo l\'aggiornamento
Sul tuo negozio è installata una versione non aggiornata dell\'estensione WooCommerce Payments. Esegui l\'aggiornamento per accettare Pagamenti di persona.
Aggiorna WooCommerce Payments
Ci sei quasi! Completa l\'impostazione di WooCommerce Payments per iniziare ad accettare pagamenti di persona.
+ Pagamenti di persona
Completa la configurazione di WooCommerce Payments nella pagina di amministrazione del negozio
Ricarica dopo l\'attivazione
L\'estensione WooCommerce Payments è installata sul tuo negozio, ma non è attivata. Attivala per accettare Pagamenti di persona.
@@ -2127,14 +2152,13 @@ Language: it
<a href=\'\'>Scopri di più</a> sull\'accettazione di pagamenti con il tuo dispositivo mobile e sull\'ordinamento dei lettori di carte
Ti serve aiuto? <a href=\'\'>Contatta il supporto</a>
Puoi comunque accettare pagamenti di persona in contanti attivando il metodo \"Pagamento alla consegna\" sul tuo negozio
- Non supportiamo Pagamenti di persona con carta in %1$s
Connessione all\'account in corso
- Pagamenti di persona
Verifica le dimensioni e il peso del pacco oppure prova a utilizzare un pacco diverso in Dettagli pacco
Nessuna tariffa di spedizione disponibile
Tutti i pacchi disponibili sono stati attivati
Attivazione del pacco in corso
Seleziona un pacco da attivare.
+ Non supportiamo Pagamenti di persona con carta in %1$s
Campo obbligatorio
Chiudi
Variante creata
@@ -2143,11 +2167,11 @@ Language: it
Genera variante
Ora che hai aggiunto gli attributi, puoi creare la tua prima variante.
Attributi creati
- %1$s%% completato
Non è raccomandato l\'annullamento dell\'aggiornamento del software in corso
Siamo spiacenti, impossibile elaborare questo pagamento
Nessuna connessione al server
Nessuna connessione a Internet
+ %1$s%% completato
Spedisci nel pacchetto originale
Aggiungi a un nuovo pacchetto
Questo elemento è attualmente in %s. Dove vorresti spostarlo?
@@ -2158,7 +2182,6 @@ Language: it
Creazione del pacchetto non riuscita. Riprova.
Creazione del pacchetto non riuscita: problema API sconosciuto.
Creazione del pacchetto non riuscita: %1$s
- Attendi…
Creazione di un nuovo pacchetto
Valore non valido.
Campo richiesto.
@@ -2172,10 +2195,11 @@ Language: it
Confezione
Scegli tipo di pacchetto
Tipo di pacchetto
- Configura il pacchetto che utilizzerai per spedire i tuoi prodotti. Lo salveremo per ordini futuri.
Aggiungi un nuovo pacchetto
Crea un nuovo pacchetto
Le dimensioni del pacchetto devono essere superiori a zero. Aggiorna le dimensioni dell\'elemento nella sezione Spedizione della pagina del prodotto per continuare.
+ Attendi…
+ Configura il pacchetto che utilizzerai per spedire i tuoi prodotti. Lo salveremo per ordini futuri.
Pacchetto originale
Dimensioni dell\'elemento
Elemento spedito singolarmente
@@ -2188,11 +2212,11 @@ Language: it
Il controllo dell\'aggiornamento della versione del software non è riuscito
<a href=\'\'>Scopri di più</a> sull\'accettazione di pagamenti con dispositivi mobili e sulla possibilità di ordinare lettori di carte
Attiva il bluetooth
- Nessun lettore connesso
Impossibile connettersi al lettore
Connetti
Sono stati trovati più lettori
L\'ordine è già stato pagato
+ Nessun lettore connesso
Grazie per l’acquisto! Fai clic sul link qui sotto per la ricevuta di pagamento.\n\n%s
Errore durante il download del modulo doganale
Stampa fattura doganale
@@ -2208,12 +2232,11 @@ Language: it
Aggiungi prodotto
Attributi della variante
Attiva il Bluetooth del dispositivo mobile
- Errore durante il recupero dell\'ordine. Lo stato nell\'ordine dell\'app potrebbe essere obsoleto.
La tua ricevuta da %s
Aggiornamento dell\'ordine
Aggiornamento dello stato dell\'app
Il cliente ha scelto %1$s
- I moduli dei clienti richiedono un numero di telefono a 10 cifre
+ Errore durante il recupero dell\'ordine. Lo stato nell\'ordine dell\'app potrebbe essere obsoleto.
Modulo dei clienti completato
Se riscontri problemi con la stampa dal tuo dispositivo, contatta l\'assistenza clienti per la tua stampante.
Se la stampa non è disponibile, puoi sempre salvare la ricevuta come PDF e inviarla via e-mail per stamparla da un altro dispositivo.
@@ -2226,6 +2249,7 @@ Language: it
Per creare una variante, dovrai prima impostare i suoi attributi (ad esempio \"Colore\", \"Dimensione\")
1 variante
%1$s varianti
+ I moduli dei clienti richiedono un numero di telefono a 10 cifre
Tracciabilità USPS
Aggiornamento del software del lettore
Aggiornamento del software
@@ -2236,7 +2260,6 @@ Language: it
Aggiorna il software del lettore
Batteria %s%%
LETTORE CONNESSO
- Connetti il lettore di carte
Accendi il lettore di carte e posizionalo accanto al dispositivo mobile
Assicurati che il lettore di carte sia carico
Connetti il tuo lettore di carte
@@ -2245,7 +2268,6 @@ Language: it
Preparazione alla ricezione del pagamento
Il valore dichiarato deve essere maggiore di zero
Il peso deve essere maggiore di zero
- Questo campo è richiesto
Descrivi che tipo di restrizioni deve avere questo pacchetto.
Descrivi che tipo di beni contiene questo pacchetto.
Peso (%1$s per unità)
@@ -2269,7 +2291,6 @@ Language: it
Descrizione
Contenuto del pacchetto
Il numero di transizione interno è richiesto per la spedizione a %1$s.
- Il numero di transizione interno è richiesto per gli articoli della spedizione valutati più di $ 2.500 per numero di tariffa
Formato non valido
Dettagli sulla restrizione
Dettagli sui contenuti
@@ -2277,6 +2298,9 @@ Language: it
Tipo di contenuti
Restituisci al mittente se non è possibile consegnare il pacchetto
fino a %s
+ Connetti il lettore di carte
+ Questo campo è richiesto
+ Il numero di transizione interno è richiesto per gli articoli della spedizione valutati più di $ 2.500 per numero di tariffa
Se hai abilitato questa impostazione, il cliente riceverà un\'e-mail di conferma una volta completato l\'ordine
Rivedi l\'ordine
🎉 Ordine completato.
@@ -2285,9 +2309,9 @@ Language: it
Scopri di più sui ruoli e sulle autorizzazioni
Quest\'app supporta solo i ruoli utente di Amministratore e Gestore negozio. Contatta il proprietario del negozio per aggiornare il tuo ruolo.
Modifica e aggiungi nuovi prodotti da qualsiasi luogo
+ Salta
Gestisci e modifica gli ordini in movimento
Tieni traccia delle vendite e dei prodotti che performano meglio
- Salta
Prodotto esterno
Prodotto raggruppato
Prodotto variabile
@@ -2296,9 +2320,6 @@ Language: it
Prodotto fisico semplice
Apri le impostazioni
Apri le impostazioni
- Il Bluetooth è disabilitato
- La posizione è disabilitata
- Autorizzazione della posizione richiesta mancante precisa
Impossibile connettersi al lettore.
Connessione al lettore
Connetti al lettore
@@ -2306,9 +2327,10 @@ Language: it
Ricerca di lettori
Conteggio articoli
Crea nuova etichetta di spedizione
- Prodotto virtuale semplice
+ Il Bluetooth è disabilitato
+ La posizione è disabilitata
+ Autorizzazione della posizione richiesta mancante precisa
Desideri eliminare questa variante?
- Generazione della variante
Eliminazione del prodotto
Invia ricevuta
Stampa ricevuta
@@ -2322,6 +2344,8 @@ Language: it
Impossibile visualizzare in anteprima l\'etichetta di spedizione. Installa un\'app per la visualizzazione del PDF e riprova.
Non siamo in grado di rilevare un sito WordPress all\'indirizzo inserito. Assicurati che WordPress sia installato e di disporre dell\'ultima versione disponibile.
più righe di spedizione
+ Prodotto virtuale semplice
+ Generazione della variante
Impossibile contrassegnare l\'ordine come completato
Errore durante l\'acquisto delle etichette
Attendi…
@@ -2350,18 +2374,17 @@ Language: it
Solo il proprietario del sito può gestire i metodi di pagamento dell\'etichetta di spedizione. Contatta il proprietario del negozio %1$s (%2$s) per gestire i metodi di pagamento.
Aggiungi varianti
Aggiungi variante
- Crea la prima variante
%s totali
%s tariffe selezionate
Idoneo per il requisito della firma gratuita
Idoneo per il ritiro gratuito
- Assicurazione (%s)
- tracciabilità
Includi %s
Firma di un adulto richiesta (%s)
Firma richiesta (%s)
+ Assicurazione (%s)
+ tracciabilità
Il cliente ha pagato %1$s di %2$s per la spedizione
- Quando acquisti le etichette di spedizione tramite WooCommerce, ottieni dal 5% 25 al 40% 25 di sconto rispetto alle tariffe degli uffici postali.
+ Crea la prima variante
Cos\'è lo sconto di WooCommerce Services?
Si è verificato un errore durante il caricamento delle opzioni di spedizione
Corrieri e tariffe
@@ -2379,6 +2402,7 @@ Language: it
Aggiungi ogni nome dell\'opzione e premi Invio
In alternativa, tocca per selezionare un\'opzione esistente
Nome opzione
+ Quando acquisti le etichette di spedizione tramite WooCommerce, ottieni dal 5% 25 al 40% 25 di sconto rispetto alle tariffe degli uffici postali.
Errore durante il salvataggio delle impostazioni
Attendi…
Salvataggio delle impostazioni
@@ -2401,18 +2425,15 @@ Language: it
Aggiungi attributo
Attributi
Modifica attributi
- Peso totale dei pacchi: %1$s %2$s
%1$d articoli in %2$d pacchetti
Peso del pacchetto totale: %1$s %2$s
Pacchetti personalizzati
Impossibile recuperare i prodotti
- Alcuni campi richiesti sono vuoti.
Peso non valido
Pacchetto selezionato
Attendi…
Caricamento dei pacchetti.
Pacchetto %1$d
- %d elementi
Impossibile caricare le definizioni dei pacchetti
Include il peso del pacchetto
Peso del pacchetto totale: %1$s
@@ -2425,16 +2446,17 @@ Language: it
Abbiamo leggermente modificato l\'indirizzo inserito. Se è corretto, usa l\'indirizzo inserito per garantire una consegna accurata.
Modifica indirizzo selezionato
Usa l\'indirizzo selezionato
+ Alcuni campi richiesti sono vuoti.
+ %d elementi
+ Peso totale dei pacchi: %1$s %2$s
Caricamento dei dati dell\'indirizzo
Nuove funzionalità disponibili.
- Trova sulla mappa
Contatta cliente
Strada non valida
Numero civico mancante
Indirizzo non trovato
Impossibile verificare automaticamente l\'indirizzo di spedizione. Visualizzalo su Google Maps o prova a contattare il cliente per assicurarti che l\'indirizzo sia corretto.
Convalida dell\'indirizzo non riuscita
- Attendi…
Convalida dell\'indirizzo in corso
Impossibile caricare i dati dell\'indirizzo
Usa l\'indirizzo inserito
@@ -2445,6 +2467,8 @@ Language: it
Telefono
Società
Nome
+ Attendi…
+ Trova sulla mappa
App di Google Maps trovata
Attendi…
Siamo spiacenti, la rimozione delle immagini sulle varianti dei prodotti è supportata in WooCommerce 4.7 o versione superiore.
@@ -2460,31 +2484,30 @@ Language: it
Dettagli imballaggio
Crea etichetta di spedizione
Ulteriori informazioni
- Evita la fila all\'ufficio postale stampando le etichette di spedizione a casa dal tuo dispositivo mobile a prezzi scontati.
Risparmia tempo e denaro con WooCommerce Shipping
WooCommerce Shipping
Contrassegna ordine come completo
- Scopri di più su come creare etichette dal dispositivo mobile
Crea etichetta di spedizione
- Ora puoi creare etichette di spedizione per tutti gli ordini fisici direttamente dal tuo dispositivo con il plugin WooCommerce Shipping gratuito. Tocca \"Crea etichetta di spedizione\" per provare la funzionalità beta.
Crea etichette di spedizione dal tuo dispositivo.
+ Ora puoi creare etichette di spedizione per tutti gli ordini fisici direttamente dal tuo dispositivo con il plugin WooCommerce Shipping gratuito. Tocca \"Crea etichetta di spedizione\" per provare la funzionalità beta.
+ Evita la fila all\'ufficio postale stampando le etichette di spedizione a casa dal tuo dispositivo mobile a prezzi scontati.
+ Scopri di più su come creare etichette dal dispositivo mobile
+ Modifica
Tariffe
Importo di pagamento netto
Pagato
Scopri di più su come connettere Jetpack
- Modifica
Convalida
Trascina e rilascia per riordinare le foto
+ Elimina
Impostazioni download
Inserisci un nome valido
Inserisci URL file
- Libreria multimediale WordPress
Verifica che l\'URL inserito sia valido
Attendi…
Caricamento file
Errore durante il caricamento del file
Aggiungi file scaricabile
- Aggiungi file scaricabile da
Includi i file scaricabili con gli acquisti
Annulla
Sì, modifica
@@ -2493,7 +2516,6 @@ Language: it
File
Desideri rimuovere questo file?
Prodotto scaricabile
- Elimina
Scadenza download
Limite di download
Inserisci il numero di giorni prima della scadenza di un link di download oppure lascia vuoto il campo se non scade mai
@@ -2508,11 +2530,13 @@ Language: it
Potrebbe essere necessario <b>configurare la stampa Wi-Fi direttamente sulla stampante stessa</b>. Assicurati che il firmware della stampante sia aggiornato e consulta la documentazione della stampante per le istruzioni.
Puoi selezionare il <b> servizio di stampa predefinito</b> del dispositivo oppure installare l\'<b>app della stampante </b> (dovrebbe apparire come opzione consigliata)
Assicurati che stampante e dispositivo siano collegati alla <b>stessa rete Wi-Fi</b>
- Prova la nuova funzionalità di creazione di prodotti semplici, collegati e raggruppati mentre ci prepariamo al lancio
+ Libreria multimediale WordPress
+ Aggiungi file scaricabile da
Incrementa le vendite con upsell e cross-sell
Modifica prodotti
Aggiungi prodotti
Prodotti promossi nel carrello quando viene selezionato il prodotto corrente
+ Prova la nuova funzionalità di creazione di prodotti semplici, collegati e raggruppati mentre ci prepariamo al lancio
Cross-sell
Prodotti promossi al posto del prodotto attualmente visualizzato (ossia i prodotti più redditizi)
Up-sell
@@ -2520,7 +2544,6 @@ Language: it
%1$s%2$s x %3$s
Ottieni un link di accesso tramite e-mail
Hmm, non riusciamo a trovare un account di WordPress.com collegato a questo indirizzo e-mail.
- Prova a visualizzare i componenti aggiuntivi per gli ordini mentre ci prepariamo a lanciarli
Creazione di prodotti
Preferenze
Errore durante lo spostamento nel cestino del prodotto
@@ -2532,24 +2555,25 @@ Language: it
L\'aggiunta delle opzioni come dimensioni e colore è attualmente disponibile solo sul web. Queste verranno visualizzate come opzioni sulla pagina del prodotto del tuo sito.
Crea nuovi prodotti dall\'app!
Prodotto non trovato
- Se stai ancora riscontrando problemi di stampa dal tuo dispositivo, puoi <b>salvare la tua etichetta come PDF</b> e inviarla via e-mail per stamparla da un altro dispositivo.
- Dopo aver selezionato <b>\"Stampa le etichette di spedizione\"</b>, potrebbe essere necessario selezionare e aggiungere una stampante se non hai stampato da questo dispositivo prima.
Opzioni formato etichetta
- Stampa dal dispositivo
Etichetta (4 x 6 pollici)
Lettera (8,5 x 11 pollici)
Nota legale (8,5 x 14 pollici)
Errore visualizzazione etichetta di spedizione
- Non sai come stampare dal dispositivo mobile?
Visualizza il layout dell\'etichetta e le opzioni delle dimensioni del foglio
Stampa etichetta di spedizione
Scegli dimensione carta
Dimensione carta
- Se hai già utilizzato l\'etichetta in un pacchetto, stamparla e utilizzarla di nuovo è una violazione dei nostri termini di servizio.
Si è verificato un errore di stampa durante l\'acquisto dell\'etichetta, puoi stamparla nuovamente.
Stiamo lavorando per rendere più semplice per te stampare le etichette di spedizione direttamente dal dispositivo. Per ora, se hai creato etichette di spedizione per questo ordine nell\'amministrazione del negozio con WooCommerce Shipping, puoi stamparle nei Dettagli dell\'ordine qui.
Stampa etichette di spedizione dal dispositivo.
+ Stampa dal dispositivo
+ Se hai già utilizzato l\'etichetta in un pacchetto, stamparla e utilizzarla di nuovo è una violazione dei nostri termini di servizio.
+ Se stai ancora riscontrando problemi di stampa dal tuo dispositivo, puoi <b>salvare la tua etichetta come PDF</b> e inviarla via e-mail per stamparla da un altro dispositivo.
+ Dopo aver selezionato <b>\"Stampa le etichette di spedizione\"</b>, potrebbe essere necessario selezionare e aggiungere una stampante se non hai stampato da questo dispositivo prima.
Stampa etichetta di spedizione
+ Prova a visualizzare i componenti aggiuntivi per gli ordini mentre ci prepariamo a lanciarli
+ Non sai come stampare dal dispositivo mobile?
\u0022%1$s\u0022
Bozza prodotto salvata
Errore durante il salvataggio della bozza del prodotto
@@ -2597,12 +2621,12 @@ Language: it
Accedi con un altro account
Seleziona il negozio da connettere
Continua con WordPress.com
- Un prodotto con varianti come colore o misura
%d prodotto selezionato
%d prodotti selezionati
Aggiungi i prodotti al gruppo
Aggiungi prodotto
Inserisci una password
+ Un prodotto con varianti come colore o misura
Torna al negozio
Contattaci qui
Tieni presente che questo non è un ticket di supporto e non saremo in grado di rispondere a feedback individuali.\n\nTi serve aiuto? %1$s
@@ -2627,8 +2651,8 @@ Language: it
Nessun prezzo impostato
Abilitata
Devi impostare il prezzo di vendita se è pianificata una vendita
- Ora puoi modificare i prodotti variabili, esterni e raggruppati, puoi cambiare il tipo di prodotto e aggiornare categorie e tag.
%1$s ha lasciato una recensione
+ Ora puoi modificare i prodotti variabili, esterni e raggruppati, puoi cambiare il tipo di prodotto e aggiornare categorie e tag.
Mi piace
Potrebbe essere migliore
Ti piace l\'app WooCommerce?
@@ -2637,24 +2661,24 @@ Language: it
Si è verificato un errore durante l\'aggiunta di tag
Aggiunta di tag
Il rimborso è in fase di elaborazione. Attendi…
- Richiesta di rimborso inviata correttamente
Rimborsa etichetta (-%1$s)
Importo idoneo al rimborso
Data di acquisto
- Puoi richiedere un rimborso per un\'etichetta di spedizione non utilizzata per spedire un pacchetto. L\'elaborazione richiederà almeno 14 giorni.
Richiedi un rimborso
Rimborsa etichetta di spedizione
+ Puoi richiedere un rimborso per un\'etichetta di spedizione non utilizzata per spedire un pacchetto. L\'elaborazione richiederà almeno 14 giorni.
+ Richiesta di rimborso inviata correttamente
Bene fisico
Un breve estratto sul tuo prodotto
Rendi i tuoi prodotti più semplici da trovare con i tag
Organizza i tuoi prodotti in gruppi correlati
+ Disabilitata
Aggiungi peso e dimensioni
Aggiungi altri dettagli
Organizza i tuoi prodotti in tag
Aggiungi il tuo primo tag
Tag
Aggiungi tag
- Disabilitata
Prodotto virtuale
Aggiungi altri dettagli
%1$s prodotto
@@ -2662,9 +2686,7 @@ Language: it
%s prodotto
Prodotti rimanenti
%1$s \u2022 %2$s
- Rimborso dell\'etichetta %1$s richiesto
Traccia spedizione
- %1$s\n%2$s
Nascondi dettagli spedizione
Mostra dettagli spedizione
Carta di credito
@@ -2674,6 +2696,8 @@ Language: it
Spedisci a
Spedisci da
Pacco %d
+ %1$s\n%2$s
+ Rimborso dell\'etichetta %1$s richiesto
SKU: %1$s
%1$s (%2$s opzioni)
Etichette di spedizione
@@ -2694,12 +2718,12 @@ Language: it
Nota sulla privacy per utenti in California
Mantieni le modifiche
Fino al giorno %1$s
- Abbiamo aggiunto più funzionalità di modifica ai prodotti! Ora puoi aggiornare immagini, vedere anteprime e condividere i prodotti.
Nuove opzioni di modifica disponibili
- Modifica limitata disponibile
+ Abbiamo aggiunto più funzionalità di modifica ai prodotti! Ora puoi aggiornare immagini, vedere anteprime e condividere i prodotti.
Prodotti
%1$s x %2$s
%1$s %2$s
+ Modifica limitata disponibile
Esterno
Semplice
Pubblicato privatamente
@@ -2789,11 +2813,11 @@ Language: it
Larghezza
Lunghezza
Prodotti rimborsati
- %1$s (%2$s x %3$d)
%1$s tramite %2$s
Desideri emettere un rimborso? Questo non può essere annullato.
Prodotti rimborsati
Rimborsi
+ %1$s (%2$s x %3$d)
Iscriviti a WordPress.com
Siamo spiacenti, non è stato possibile trovare risultati per \"%s\"
Acquisisci recensioni del prodotto di alta qualità per il tuo negozio
@@ -2814,33 +2838,32 @@ Language: it
Aggiungi inventario
Ricerca dei tuoi ordini in corso…
Inserisci testo
- Inserisci il titolo del prodotto
- Prodotto salvato
Errore durante l\'aggiornamento del prodotto
Attendi…
Descrivi il tuo prodotto
Descrizione
Modifica descrizione
+ Inserisci il titolo del prodotto
+ Prodotto salvato
+ Fatto
Desideri eliminare queste modifiche?
Aggiorna
- Fatto
Rimborso in corso, attendi…
Rimborsa spedizione
Seleziona quantità
Rimborso spedizione
Rimborso prodotti
- %1$s x %2$s ciascuno
%d elementi selezionati
Non selezionare nessuno
Seleziona tutto
In attesa della conferma del rimborso…
+ %1$s x %2$s ciascuno
Ridimensiona e comprimi le immagini per un caricamento più rapido
Ottimizzazione delle immagini
Scatta una foto
Scegli da dispositivo
Seleziona un metodo di caricamento
Carica
- Caricamento delle immagini in corso…%1$d di %2$d
Caricamento delle immagini in corso…
Impossibile accedere alla fotocamera
Desideri rimuovere questa immagine?
@@ -2855,6 +2878,7 @@ Language: it
Aggiungi immagine
In arrivo
Rimuovi
+ Caricamento delle immagini in corso…%1$d di %2$d
Non siamo stati in grado di accedere al tuo sito. Dovrai contattare il tuo fornitore di hosting per risolvere questo problema.
Non siamo stati in grado di accedere al tuo sito perché abbiamo riscontrato un problema con il <b>certificato SSL</b>. Dovrai contattare il tuo fornitore di hosting per risolvere questo problema.
Non siamo stati in grado di accedere al tuo sito perché viene richiesta l\'<b>autenticazione HTTP</b>. Dovrai contattare il tuo fornitore di hosting per risolvere questo problema.
@@ -2863,8 +2887,8 @@ Language: it
Accedi con le credenziali del sito.
Accedi con le credenziali del sito %1$s
Invia e-mail di verifica
- Testa la nuova funzionalità di modifica del prodotto mentre ci prepariamo a lanciarla
Modifica del prodotto
+ Testa la nuova funzionalità di modifica del prodotto mentre ci prepariamo a lanciarla
Si è verificato un errore durante il recupero del tuo account. Puoi riprovare ora o chiudere e riprovare più tardi.
Si è verificato un errore. Accedi per continuare
Connessione al tuo sito in corso…
@@ -2899,15 +2923,12 @@ Language: it
Nessun prodotto corrispondente
Ancora nessun prodotto
%s in magazzino
- \u2022 %d varianti in magazzino
Immagine prodotto
%1$s ha lasciato una recensione per %2$s
Non approvato
Errore durante il recupero della nuova recensione prodotto
Errore durante il recupero delle recensioni prodotto
- Si è verificato un problema con il rimborso. Riprova.
- Il rimborso è stato emesso correttamente.
- Il rimborso per %s è in fase di elaborazione. Attendi…
+ \u2022 %d varianti in magazzino
Icona preventivo
Rimborso manuale
Dettagli di rimborso
@@ -2925,6 +2946,9 @@ Language: it
Rimborso %s
Disponibile per il rimborso: %s
Emetti rimborso
+ Si è verificato un problema con il rimborso. Riprova.
+ Il rimborso è stato emesso correttamente.
+ Il rimborso per %s è in fase di elaborazione. Attendi…
%1$s tramite %2$s
Statistiche migliorate
Funzionalità beta
@@ -2938,12 +2962,12 @@ Language: it
Statistiche odierne
Registrati
Hai già Jetpack? %1$s
- Tentativo di accesso con Jetpack in corso…
aggiorna l\'app per continuare
- Per usare quest\'app per %1$s dovrai avere il plugin Jetpack impostato e connesso a questo account. \n\nUna volta impostato, aggiorna l\'app
Prova un altro negozio
Database declassato, ricreazione delle tabelle e caricamento dei negozi in corso
Caricamento dei negozi in corso
+ Tentativo di accesso con Jetpack in corso…
+ Per usare quest\'app per %1$s dovrai avere il plugin Jetpack impostato e connesso a questo account. \n\nUna volta impostato, aggiorna l\'app
Nessun corriere trovato
Inserisci un indirizzo di sito Web completo, come esempio.com.
Ancora nessuna recensione!
@@ -2954,12 +2978,11 @@ Language: it
Impossibile recuperare le impostazioni: alcune API non sono disponibili per questa combinazione di ID app OAuth + account.
Stiamo assumendo.
Copia numero di tracciabilità
- Verifica per WooCommerce…
aggiorna l\'app
+ Verifica per WooCommerce…
Nessun indirizzo specificato
Ti serve aiuto per trovare l\'e-mail con cui ti sei collegato?
Il sito web su questo indirizzo non è un sito WordPress. Per poterci collegare a esso, il sito deve avere WordPress installato.
- Accedi con WordPress.com per collegarti a <b>%1$s</b>
Zimbabwe
Zambia
Yemen
@@ -3096,6 +3119,7 @@ Language: it
Giamaica
Costa d\'Avorio
Italia
+ Accedi con WordPress.com per collegarti a <b>%1$s</b>
Israele
Isola di Man
Irlanda
@@ -3202,24 +3226,15 @@ Language: it
Afghanistan
Isole Åland
Recensione
- Corriere personalizzato
Personalizza
- Inserisci il nome di un corriere
Inserisci un numero di tracciabilità
- Seleziona un corriere
Vuoi davvero eliminare questa tracciabilità?
Impossibile aggiungere la tracciabilità
Tracciabilità della spedizione aggiunta
- Errore durante il recupero dei corrieri
- Corriere della spedizione selezionato
- Corrieri della spedizione
Data di spedizione
Inserisci link di tracciabilità
- Inserisci il nome del corriere
Inserisci numero di tracciabilità
- Seleziona il corriere
Link di tracciabilità (opzionale)
- Nome del corriere
Numero di tracciabilità
Corriere
Aggiungi tracciabilità
@@ -3232,19 +3247,25 @@ Language: it
Traccia spedizione
Nell\'amministrazione del sito puoi trovare l\'e-mail che hai usato per collegarti a WordPress.com dalla %1$sBacheca di Jetpack%2$s sotto %3$sConnessioni > Connessione dell\'account%4$s
Quale e-mail uso per accedere?
- Hai bisogno di aiuto per trovare l\'e-mail richiesta?
Jetpack è un plugin WordPress gratuito che collega il tuo negozio con gli strumenti necessari per assicurarti la migliore esperienza mobile, incluse le notifiche push e le statistiche
Che cos\'è Jetpack?
Visualizza i negozi collegati
- Sembra che %1$s sia connesso a un account WordPress.com differente.
Continua a modificare
+ Corriere personalizzato
+ Inserisci il nome di un corriere
+ Seleziona un corriere
+ Errore durante il recupero dei corrieri
+ Corriere della spedizione selezionato
+ Corrieri della spedizione
+ Inserisci il nome del corriere
+ Seleziona il corriere
+ Nome del corriere
+ Sembra che %1$s sia connesso a un account WordPress.com differente.
+ Hai bisogno di aiuto per trovare l\'e-mail richiesta?
Accedi con i tuoi nome utente e password.
Accedi utilizzando il tuo nome utente di WordPress.com invece del tuo indirizzo e-mail.
Il sito a questo indirizzo non è un sito WordPress. Per poterci connettere, il sito deve utilizzare WordPress.
Centro assistenza
- Virtuale
- Raggruppato
- Variabile
Permetti, ma invia notifica al cliente
Permetti
Non permettere
@@ -3252,6 +3273,9 @@ Language: it
Esaurito
In magazzino
Leggi altro
+ Raggruppato
+ Variabile
+ Virtuale
Impossibile caricare le immagini
Bozza
Privato
@@ -3297,11 +3321,11 @@ Language: it
Prova ora
OK
Tocca per passare da un negozio all’altro
- Seleziona negozio
Esci da questo account
Modifica lo stato dell\'ordine
Fai clic per modificare lo stato dell\'ordine
Applica
+ Seleziona negozio
No, grazie
Più tardi
Valuta ora
@@ -3315,11 +3339,11 @@ Language: it
Aggiorna il negozio su WooCommerce 3.5
Impossibile eseguire la connessione a %s
Rimuovi
- Errore durante il contrassegno di tutte le recensioni come lette
Contrassegna tutte come lette
Messaggio
Chiamata
Chiama o manda un messaggio al cliente
+ Errore durante il contrassegno di tutte le recensioni come lette
Errore durante l\'aggiornamento dello stato della recensione del prodotto
Errore durante il caricamento dei dettagli della recensione del prodotto
Cestino
@@ -3332,16 +3356,16 @@ Language: it
Gestisci notifiche
Notifiche
Desideri uscire dall\'account %s?
- Recensione contrassegnata come %1$s
Se disabilitata, la nota diventerà privata
+ Recensione contrassegnata come %1$s
Errore durante il recupero dell\'ordine
Indietro
Avvisi delle recensioni del prodotto
Avvisi dei nuovi ordini
Al cliente
- Verifica del sito in corso…
Aggiorna istruzioni
Ricerca
+ Verifica del sito in corso…
Aggiorna
e altre %d.
%d nuove notifiche
@@ -3373,9 +3397,9 @@ Language: it
Rapporti di arresto anomalo
Condividi
Versione %s
- Password HTTP
- HTTP nome utente
- Autorizzazione richiesta
+ Abbiamo effettuato troppi tentativi di invio di un codice di verifica SMS: prenditi una pausa e richiedine uno nuovo tra un minuto.
+ Non è presente alcun account WordPress.com corrispondente a questo account Google.
+ Accedi all\'account WordPress.com che hai utilizzato per connettere Jetpack.
Link inviato
Registrazione via e-mail
Verifica del codice
@@ -3384,32 +3408,9 @@ Language: it
Accesso tramite link
Accesso tramite indirizzo sito
Accesso tramite indirizzo e-mail
- Si è verificato un errore.
- Fornisci un codice di autenticazione per continuare.
- Controlla due volte la tua password per continuare.
- Accesso interrotto
- Attendi mentre stai effettuando l\'accesso.
- Accesso in corso…
- Tocca per continuare.
- Accesso effettuato!
- Si è verificato un errore di rete. Controlla la connessione e riprova.
- Inserisci un sito web WordPress.com o un sito web WordPress.com ospitato personalmente connesso a Jetpack.
- Non è possibile connettersi. Abbiamo ricevuto un messaggio di errore 403 durante il tentativo di accedere\n all\'endpoint XMLRPC del tuo sito. Per l\'app questo è necessario per comunicare con il tuo sito. Contatta il tuo host per risolvere\n questo problema.
- Non è possibile connettersi. Il tuo host sta bloccando le richieste POST ma queste sono necessarie al corretto funzionamento dell\'app\n per comunicare con il tuo sito. Contatta il tuo fornitore del servizio di hosting per risolvere questo problema.
- Non è possibile connettersi. Sul server mancano i necessari metodi XML-RPC
- Controlla che l\'URL del sito sia valido
- Si è verificato un errore
- Password dimenticata?
- Inserisci un indirizzo e-mail valido
- Verifica della email
- Accedi di nuovo per continuare.
- Accedi all\'account WordPress.com che hai utilizzato per connettere Jetpack.
- Impossibile recuperare il profilo
- È stato rilevato un sito duplicato.
- Il sito esiste già nell\'app, non lo puoi aggiungere.
- Il nome utente o la password immessi non sono corretti
- Tempo di risposta di Google troppo lungo. Potresti dover attendere fino a quando non disporrai di una connessione Internet più forte.
+ Non hai un account? %1$sRegistrati%2$s
Registrazione con Google…
+ Tempo di risposta di Google troppo lungo. Potresti dover attendere fino a quando non disporrai di una connessione Internet più forte.
Registrati con Google
Registrati con l\'e-mail
Registrandoti, accetti i nostri %1$sTermini di servizio%2$s.
@@ -3419,20 +3420,54 @@ Language: it
Si è verificato un errore durante l\'invio dell\'email. Puoi riprovare ora o chiudere e riprovare più tardi.
Per creare il tuo nuovo account WordPress.com, inserisci l\'indirizzo e-mail.
Si è verificato un errore durante il controllo dell\'indirizzo e-mail.
- \nPuoi provare un account diverso?
+ Si è verificato un errore.
+ Fornisci un codice di autenticazione per continuare.
+ Controlla due volte la tua password per continuare.
+ Accesso interrotto
+ Attendi mentre stai effettuando l\'accesso.
+ Accesso in corso…
+ Tocca per continuare.
+ Accesso effettuato!
Il login tramite Google non può essere inizializzato.
- Abbiamo effettuato troppi tentativi di invio di un codice di verifica SMS: prenditi una pausa e richiedine uno nuovo tra un minuto.
+ Inserisci una password
+ \nPuoi provare un account diverso?
Si sono verificati dei problemi con la connessione con l’account Google.
- Non è presente alcun account WordPress.com corrispondente a questo account Google.
Chiudi
Accedi con Google.
+ Si è verificato un errore di rete. Controlla la connessione e riprova.
Autenticato come
Impossibile rilevare il client di posta elettronica della tua app
- Non hai un account? %1$sRegistrati%2$s
Inserisci un codice di verifica
- Inserisci una password
- Inserisci un nome utente
+ È stato rilevato un sito duplicato.
+ Il sito esiste già nell\'app, non lo puoi aggiungere.
+ Non è possibile connettersi. Abbiamo ricevuto un messaggio di errore 403 durante il tentativo di accedere\n all\'endpoint XMLRPC del tuo sito. Per l\'app questo è necessario per comunicare con il tuo sito. Contatta il tuo host per risolvere\n questo problema.
+ Non è possibile connettersi. Il tuo host sta bloccando le richieste POST ma queste sono necessarie al corretto funzionamento dell\'app\n per comunicare con il tuo sito. Contatta il tuo fornitore del servizio di hosting per risolvere questo problema.
+ Verifica della email
+ Non è possibile connettersi. Sul server mancano i necessari metodi XML-RPC
+ Impossibile recuperare il profilo
+ Accedi di nuovo per continuare.
+ Password dimenticata?
+ Il nome utente o la password immessi non sono corretti
+ Inserisci un indirizzo e-mail valido
+ Si è verificato un errore
+ Autorizzazione richiesta
+ Controlla che l\'URL del sito sia valido
+ Password HTTP
+ HTTP nome utente
+ Inserisci un sito web WordPress.com o un sito web WordPress.com ospitato personalmente connesso a Jetpack.
+ In alternativa:
+ Generale
+ \@%s
+ Accedi con il tuo nome utente.
+ Accedi inserendo l\'indirizzo del tuo sito.
+ Inviami invece un messaggio con un altro codice.
+ Abbiamo inviato un messaggio di testo al numero di telefono che termina in %s. Inserisci il codice di verifica contenuto nell\'SMS.
+ Per procedere con questo account Google, fornisci la password WordPress.com corrispondente. Verrà richiesta solo una volta.
Accedi a WordPress.com per condividere il contenuto.
+ Immetti l\'indirizzo del tuo sito WordPress sul quale desideri condividere il contenuto.
+ Si è verificato un errore durante l\'apertura di un browser web di base. Scegli un\'altra applicazione:
+ Impossibile aprire il link
+ Inserisci un nome utente
Accedi a WordPress.com per accedere all\'articolo.
Si è verificato un errore durante l\'aggiunta del sito. Codice di errore: %s
Verifica indirizzo sito
@@ -3441,25 +3476,15 @@ Language: it
Qual è l\'indirizzo del mio sito?
Hai bisogno di aiuto per trovare l\'indirizzo del tuo sito?
Indirizzo sito
- Immetti l\'indirizzo del tuo sito WordPress sul quale desideri condividere il contenuto.
\@%s
Hai già effettuato l\'accesso a WordPress.com
Continua
- Connettiti a un sito
Connetti un altro sito
- Per procedere con questo account Google, fornisci la password WordPress.com corrispondente. Verrà richiesta solo una volta.
Inserisci la tua password WordPress.com.
- Attualmente non disponibile. Inserisci la tua password
Richiesta e-mail di accesso
Sembra che la password non sia corretta. Verifica attentamente le informazioni e riprova.
Richiesta codice di verifica tramite SMS.
- Inviami invece un messaggio con un altro codice.
Inviami un codice.
- Abbiamo inviato un messaggio di testo al numero di telefono che termina in %s. Inserisci il codice di verifica contenuto nell\'SMS.
- Ci sei quasi! Inserisci il codice di verifica per WordPress.com dalla tua app di autenticazione.
- Accedi con il tuo nome utente.
- Accedi inserendo l\'indirizzo del tuo sito.
- In alternativa:
Apri mail
Successivo
Gestisci il tuo sito con Jetpack mentre sei in movimento: hai WordPress in tasca.
@@ -3467,29 +3492,39 @@ Language: it
Tieni il passo con i tuoi siti preferiti e partecipa a una conversazione ovunque e in qualsiasi momento.
Guarda i lettori da tutto il mondo leggere e interagire con il tuo sito, in tempo reale.
Pubblica dal parco. Lavora al blog sull\'autobus. Commenta dal bar. WordPress si sposta con te.
- Accedi
- Aiuto
- Password
- Nome utente
- Oppure inserisci la tua password
+ Sei già connesso a un account WordPress.com, non puoi aggiungere un sito WordPress.com legato a un altro account.
+ Riprova
+ Uscire
Invia link
+ Attualmente non disponibile. Inserisci la tua password
+ Sto entrando
+ Oppure inserisci la tua password
+ Indirizzo e-mail
+ Dettagli
+ Annulla
Codice di verifica non valido
Codice di verifica
- Indirizzo e-mail
+ Aiuto
+ Rimuovi
+ Accedi
+ Nome utente
+ Password
+ Senza titolo
+ Impostazioni
+ Oggi
+ Annulla
Supporto %s WooCommerce per Android
opzione non controllata
opzione controllata
Politica sulla privacy di terze parti
Politica sui cookie
Politica sulla privacy
- Creato con amore da Automattic. %1$s
Utilizziamo altri strumenti di tracciamento, inclusi alcuni di terze parti. Leggi le informazioni sugli strumenti e come utilizzarli.
Leggi la politica sulla privacy
Questa informazione ci aiuta a migliorare i nostri prodotti, a rendere il marketing più mirato, a personalizzare la tua esperienza su WooCommerce e molto altro come descritto in dettaglio nostra politica sulla privacy
Condividi informazioni con gli strumenti di analisi relative all\'uso dei servizi mentre sei connesso al tuo account WordPress
Raccogli informazioni
Impostazioni privacy
- Impostazioni
Stato dell\'ordine
Rimborsato
Cancellato
@@ -3503,7 +3538,6 @@ Language: it
Aggiungi
Nota e-mail al cliente
Errore durante la modifica dell\'ordine
- Errore durante il recupero delle note
Ordine contrassegnato come completo
Contrassegna ordine come completo
Aggiungi una nota dell\'ordine
@@ -3512,7 +3546,6 @@ Language: it
Mostra fatturazione
Pagamento cancellato
Note ordine
- Privato
Scrivi una nota dell\'ordine
Immagine profilo personalizzata
Nota fornita dai clienti
@@ -3537,9 +3570,6 @@ Language: it
Nessun ordine
Visualizza ordini
Visualizza ordine
- Nessuna attività per questo periodo
- Ordini totali: %s
- Immagine dell\'errore
Errore durante recupero dati
Fatturato
Ordini
@@ -3552,17 +3582,11 @@ Language: it
Nessun negozio WooCommerce
Foto del profilo
Negozio collegato
- Leggi le %1$sistruzioni di configurazione%2$s.
Questa app richiede che Jetpack sia connesso al tuo negozio.
- \@%s
- Immetti l\'indirizzo del negozio WooCommerce che desideri connettere.
Accedi con l\'indirizzo e-mail dell\'account WordPress.com per gestire i negozi WooCommerce.
- Sei già connesso a un account WordPress.com, non puoi aggiungere un sito WordPress.com legato a un altro account.
- Impossibile aprire il link
Nessuna app per SMS trovata
Nessuna app per e-mail trovata
Nessuna app per telefono trovata
- Si è verificato un errore durante l\'apertura di un browser web di base. Scegli un\'altra applicazione:
Impossibile aprire il link
%1$s alle %2$s
Più vecchie di un mese
@@ -3571,22 +3595,15 @@ Language: it
Ieri
Oggi
Prodotti
- Rimuovi
Quest\'anno
Questo mese
Questa settimana
- Oggi
Prodotto
La rete non è disponibile. Controlla la connessione dati o WiFi.
Offline u2014 utilizza i dati memorizzati nella cache
Per saperne di più
- Annulla
- Senza titolo
Continua
- Annulla
- Riprova
Nascondi dettagli
- Dettagli
Sconto
Subtotale
Tasse
@@ -3596,11 +3613,18 @@ Language: it
%1$s%2$s
Ordini
Il mio negozio
- Uscire
- Sto entrando
Tutto
- Generale
WooCommerce
+ Immagine dell\'errore
+ Creato con amore da Automattic. %1$s
+ Immetti l\'indirizzo del negozio WooCommerce che desideri connettere.
+ Privato
+ Connettiti a un sito
+ Nessuna attività per questo periodo
+ Ordini totali: %s
+ Errore durante il recupero delle note
+ Leggi le %1$sistruzioni di configurazione%2$s.
+ Ci sei quasi! Inserisci il codice di verifica per WordPress.com dalla tua app di autenticazione.
- @string/date_timeframe_custom
- @string/date_timeframe_today
diff --git a/WooCommerce/src/main/res/values-ja/strings.xml b/WooCommerce/src/main/res/values-ja/strings.xml
index a7b9980e701..b3e7660fac2 100644
--- a/WooCommerce/src/main/res/values-ja/strings.xml
+++ b/WooCommerce/src/main/res/values-ja/strings.xml
@@ -1,11 +1,71 @@
+ 間違ったキー: 先頭の「_」文字を削除してください。
+ このキーはすでに別のカスタムフィールドで使用されています。\n現在、アプリは複製キーの作成をサポートしていません。 必要に応じて、wp-admin を使用してキーを複製してください。
+ カスタムフィールドを追加
+ カスタムフィールドを削除しました
+ 変更を保存できませんでした。もう一度お試しください
+ 変更を保存しました
+ 変更を保存中
+ インターネットに接続されていないようです。 Wi-Fi がオンになっていることをご確認ください。 モバイルデータを使用している場合、デバイスの設定で有効になっていることを確認してください。
+ スキャンに失敗しました。 後でもう一度お試しください
+ 値
+ キー
+ バリエーションや仮想などの他の商品タイプは、今後のアップデートで利用可能になります。
+ 現在、POS で使用できるのはシンプルな物理的商品のみです。
+ キャンセル
+ 期間
+ キャンペーンは停止するまで実行されます。
+ 期間を特定
+ %1$sまで
+ 予約
+ 1日あたりの支出
+ キャンペーンに費やす費用はいくらですか ? また、キャンペーンの期間はどの程度ですか ?
+ %1$s ➔ %2$s
+ Blaze で商品を何百万もの人に見てもらい、売上を伸ばしましょう
+ 売上を伸ばしたいとお考えですか ?
+ カスタムフィールドの読み込み中にエラーが発生しました
+ カスタムフィールド
+ 背景を暗くしました。 タップしてダイアログを閉じます。
+ 毎週%1$s
+ 止めるまで実行します
+ %1$sから進行中
+ 週の支出
+ 毎週%1$s、%2$sに開始
+ 毎週
+ 残り
+ 合計
+ クリックスルー
+ 端末がバッテリーセーバーモードになっているようです。 \nストアがアクティブな間はストア情報を提供できません
+ オプション付きのポップアップメニュー。 スワイプしてアイテム間を移動します。
+ ツールバーメニューを開く
+ カードリーダーステータス付きのツールバー。 メニューが開いています。 ダブルタップして操作します。
+ カードリーダーステータス付きのツールバー。 ダブルタップして操作します。
+ メニューが無効です
+ メニューが有効です
+ カードリーダーが接続されていません。 ダブルタップして接続
+ カードリーダーが接続されました
+ 背景を暗くしました。 タップしてメニューを閉じます。
+ 支払い成功チェックマークアイコン
+ お買い物カゴからこのアイテムを削除
+ 進行中の注文はすべて失われます。
+ 販売時点管理モードを終了しますか ?
+ 閉じる
+ 背景を暗くしました。 タップしてダイアログを閉じます。
+ ダブルタップしてダイアログを閉じる
+ シンプルな商品のみのダイアログ
+ ダブルタップして詳しく確認
+ シンプルな商品のみのバナー
+ Blaze 広告で商品を宣伝し、今すぐ売上を増やしましょう。
+ 売上を増やす
+ 外出先で支払いを受け取る
+ 誤った PIN が入力されました。 再度お試しいただくか、別の支払い方法をお使いください
メニュー
お買い物カゴ %s の商品、価格%s
商品 %s、価格%s
@@ -14,8 +74,7 @@ Language: ja_JP
新しい注文
OK
+ ストア管理で注文を作成
- シンプルな商品以外の支払いを受け取るには、POS を終了して注文タブから新しい注文を作成します。
- 現在、POS で使用できるのはシンプルな物理的商品のみです。\nバリエーションや仮想などの他の商品タイプは、今後のアップデートで利用可能になります。
+ シンプルな商品以外の支払いを受け取るには、POS を終了して注文タブから新しい注文を作成します。
商品が表示されないのはなぜですか ?
情報
閉じる
@@ -39,8 +98,6 @@ Language: ja_JP
現在、POS はシンプルな商品のみに対応しています – \n作成して開始します。
サポート対象の商品が見つかりません
商品なし
- お客様にサービスを提供しましょう
- 開始
サポートを受ける
リーダーを接続
写真が削除されました
@@ -135,8 +192,7 @@ Language: ja_JP
数量ルールなし
読者
キャンセル
- 終了
- POS を終了してもよいですか ?
+ 終了
POS を終了
%s をお買い物カゴから削除
購入手続き
@@ -419,20 +475,16 @@ Language: ja_JP
キャッチフレーズ
画像を変更
適用
- 開始
+ 開始日
%1$s日間
- 期間を設定する
インプレッションには、広告が潜在顧客に表示される頻度が反映されます。\n\n\n オンライントラフィックやユーザーの行動は変動するため正確な数を保証することはできないものの、広告の実際のインプレッションを目標の数に可能な限り近づけることを目指します。\n\n\n インプレッションは可視性のことであり、閲覧者が取ったアクションではありません。
完了
インプレッション
更新
編集
- 期間
1日あたりの推定リーチ数
1日 %1$s
%1$s日間
- 総支出
- 商品プロモーションキャンペーンにいくら使いますか ?
予算を設定する
すべて
%2$sから%1$s日間
@@ -634,7 +686,6 @@ Language: ja_JP
Blaze でストアの売上を増やす
キャンペーンのリストを更新中にエラーが発生しました。 後でもう一度お試しください。
メディアソースを選択する
- 商品名と説明文の生成中にエラーが発生しました。
テキストが検出されませんでした。 別のパッケージ写真を選択するか、商品の詳細を手動で入力してください。
商品を追加する
バーコードをスキャンする
@@ -656,9 +707,6 @@ Language: ja_JP
デビットカードまたはクレジットカードで %s の支払いを試します。\n完了したら返金されます。
簡単、安全でプライバシーは保護されます。
あらゆる種類のオフラインでの支払いを\nスマートフォンで受け取れます。 追加のハードウェアは不要です。
- 予算
- クリック数
- インプレッション
却下
完了
有効
@@ -691,46 +739,23 @@ Language: ja_JP
設定
メールアドレスを使用して手動で詳細を追加
アプリログインのリクエストを処理できませんでした
- 商品名をクリップボードにコピーする際にエラーが発生しました。
- 商品名がクリップボードにコピーされました。
- AI によって生成された商品名
商品説明をクリップボードにコピーする際にエラーが発生しました。
商品説明がクリップボードにコピーされました。
商品の生成に失敗しました。 もう一度お試しください
住所をクリアしてこのレートの使用を停止する
この注文に新しい税率を設定する
税率の自動加算
- 商品名の生成中に問題が発生しました。 もう一度お試しください。
- 再生成
- ご記入ください
- 商品の概要とその特徴的な点を教えてください。
- AI が魅力的なタイトルを生成します
- 商品名
すべての商品レビューを表示するには、未読フィルターを無効にしてみてください
未読の商品レビューはありません
- トーンが選択されました
説得力がある
花
フォーマル
カジュアル
- トーンと声を設定してブランドに合わせた商品のプレゼンテーションを形成します。
トーンと声
詳細
- 商品説明
商品名
- 以下の情報は後からいつでも変更できます。
プレビュー
- 商品情報を作成
- トーンと声を設定する
- 主要な機能や利点、詳細を追加して商品をオンラインで見つけられるようにします。
例: やわらかな生地、丈夫な縫製、ユニークなデザインなど
- 商品のユニークな点を強調し、AI に魔法をかけてもらいましょう。
- 商品について
- 名前を提案
- 例: やわらかな生地、丈夫な縫製、ユニークなデザインなど。
- 商品名
- または、タップして選択肢を拡張し、さらに名前の候補を表示します。
- 商品名を追加する
AI を活用しています。 <a href=\'guidelines\'><u>さらに詳しく</u></a>。
手動で商品と詳細を追加します
手動で追加する
@@ -1054,7 +1079,6 @@ Language: ja_JP
特別オファーで売上を伸ばす
ストアを表示
最新の状態に保つ
- モバイル決済を利用する
管理画面でさらに管理する
一般
設定
diff --git a/WooCommerce/src/main/res/values-ko/strings.xml b/WooCommerce/src/main/res/values-ko/strings.xml
index 67b0a987e4a..a18502da281 100644
--- a/WooCommerce/src/main/res/values-ko/strings.xml
+++ b/WooCommerce/src/main/res/values-ko/strings.xml
@@ -1,11 +1,71 @@
+ 유효하지 않은 키: 처음의 \"_\" 문자를 제거하세요.
+ 이 키는 이미 다른 사용자 정의 필드에 사용되었습니다.\n이 앱은 현재 중복 키 생성을 지원하지 않습니다. 필요한 경우 wp-admin을 사용하여 키를 복제하세요.
+ 사용자 정의 필드 추가
+ 사용자 정의 필드 삭제됨
+ 변경 사항을 저장하지 못했습니다. 다시 시도하세요.
+ 변경 사항 저장됨
+ 변경 사항 저장 중
+ 인터넷에 연결되지 않은 것 같습니다. 와이파이가 켜졌는지 확인하세요. 모바일 데이터를 사용 중이라면 기기 설정에서 활성화되었는지 확인하세요.
+ 스캔하지 못했습니다. 나중에 다시 시도하세요.
+ 값
+ 키
+ 변형 상품, 가상 상품 등의 다른 상품 유형은 향후 업데이트를 통해 지원될 예정입니다.
+ 현재 POS에서는 단순 실물 상품만 사용할 수 있습니다.
+ 취소
+ 지속기간
+ 중지하실 때까지 캠페인이 실행됩니다.
+ 지속기간 지정
+ 종료 %1$s
+ 일정
+ 일일 지출
+ 캠페인에 얼마를 지출하고 얼마 동안 캠페인을 실행하시겠어요?
+ %1$s ➔ %2$s
+ Blaze를 통해 수백만 명에게 상품을 보여주고 매출 증대
+ 매출 증대에 대해 생각 중이신가요?
+ 사용자 정의 필드를 로드하는 중 오류 발생
+ 사용자 정의 필드
+ 어두워진 배경입니다. 눌러서 대화 상자 무시하기
+ 주당 %1$s
+ 사용자가 중지할 때까지 실행
+ %2$s부터 주당 %1$s
+ 주당
+ 나머지
+ 합계
+ 클릭 통과
+ 회원님의 기기가 배터리 세이버 모드인 것 같습니다. \n이 모드가 활성화된 상태에서는 스토어 정보를 제공해 드릴 수 없습니다.
+ %1$s부터 계속 진행
+ 주당 지출
+ 옵션이 포함된 팝업 메뉴입니다. 밀어서 아이템을 탐색할 수 있습니다.
+ 도구 모음 메뉴 열기
+ 카드 리더 상태를 포함한 도구 모음입니다. 메뉴가 열립니다. 두 번 눌러 상호작용할 수 있습니다.
+ 카드 리더 상태를 포함한 도구 모음입니다. 두 번 눌러 상호작용할 수 있습니다.
+ 메뉴 비활성화됨
+ 메뉴 활성화됨
+ 카드 리더가 연결되어 있지 않습니다. 두 번 눌러 연결할 수 있습니다.
+ 카드 리더가 연결되었습니다.
+ 어두워진 배경입니다. 눌러서 메뉴를 닫을 수 있습니다.
+ 결제 성공 체크 표시 아이콘
+ 장바구니에서 이 아이템 빼기
+ 진행 중인 주문이 모두 손실됩니다.
+ 판매 지점 모드를 종료할까요?
+ 닫기
+ 어두워진 배경입니다. 눌러서 대화 상자 무시하기
+ 두 번 눌러 대화 상자 무시하기
+ 단순 상품만 해당 대화 상자
+ 두 번 눌러 자세히 알아보기
+ 단순 상품만 해당 배너
+ 지금 Blaze 광고를 통해 제품을 홍보하고 매출을 늘리세요.
+ 매출 증대
+ 이동 중 결제 처리
+ 올바르지 않은 PIN이 입력되었습니다. 다시 시도하거나 다른 결제 수단을 사용하세요.
메뉴
장바구니의 상품 %s, 가격 %s
상품 %s, 가격 %s
@@ -14,14 +74,13 @@ Language: ko_KR
새 주문
확인
+ 스토어 관리에서 주문 생성
- 단순 상품이 아닌 상품에 대한 결제를 받으려면 POS를 종료하고 주문 탭에서 새 주문을 생성하세요.
- 현재 POS에서는 단순 실물 상품만 사용할 수 있습니다.\n변형 상품, 가상 상품 등의 다른 상품 유형은 향후 업데이트를 통해 지원될 예정입니다.
왜 내 상품이 보이지 않나요?
정보
닫기
- 더 알아보기
현재 POS에는 단순 실물 상품만 호환됩니다. 변형 상품, 가상 상품 등의 다른 상품 유형은 향후 업데이트를 통해 지원될 예정입니다.
단순 상품만 표시
+ 단순 상품이 아닌 상품에 대한 결제를 받으려면 POS를 종료하고 주문 탭에서 새 주문을 생성하세요.
+ 자세히\u00A0알아보기
사이트 주소
우커머스용 Google
유료 캠페인 추가
@@ -39,8 +98,6 @@ Language: ko_KR
다시 시도
지원되는 상품을 찾을 수 없음
상품 없음
- 고객에게 서비스 제공하기
- 시작하기
고객 지원
리더 연결
사진 제거됨
@@ -135,11 +192,10 @@ Language: ko_KR
세금
잠재 고객
취소
- 종료
- POS를 종료하시겠어요?
POS 종료
체크아웃
장바구니에서 %s 삭제
+ 종료
리더 상태 알 수 없음
체크아웃
리더 연결됨
@@ -419,23 +475,19 @@ Language: ko_KR
태그라인
이미지 변경
적용
- 시작
%1$s일
- 기간 설정
완료
노출 수
업데이트
+ 노출 수는 잠재적 고객에게 광고가 표시되는 빈도를 반영합니다.\n\n\n 온라인 트래픽과 사용자 행동의 유동성으로 인해 정확한 수는 보장할 수 없지만 광고의 실제 노출 수를 목표 수에 최대한 가깝게 일치시키는 것을 목표로 합니다.\n\n\n 노출 수는 독자의 행동이 아니라 가시성을 나타낸다는 점에 유의하세요.
편집
- 지속기간
일일 예상 도달 인원
- 노출 수는 잠재적 고객에게 광고가 표시되는 빈도를 반영합니다.\n\n\n 온라인 트래픽과 사용자 행동의 유동성으로 인해 정확한 수는 보장할 수 없지만 광고의 실제 노출 수를 목표 수에 최대한 가깝게 일치시키는 것을 목표로 합니다.\n\n\n 노출 수는 독자의 행동이 아니라 가시성을 나타낸다는 점에 유의하세요.
매일 %1$s
%1$s일 동안
- 총 지출
- 상품 프로모션 캠페인에 얼마를 지출하시겠습니까?
예산 설정
모두
%2$s(으)로부터 %1$s일
+ 시작 날짜
다시 표시하지 않음
나중에 다시 알림
잠깐 시간 좀 내주시겠어요? 빠른 피드백으로 AI 도우미 기능 개선을 도와주세요.
@@ -634,7 +686,6 @@ Language: ko_KR
스토어에서 Blaze로 판매 증대 유도
캠페인 목록 새로 고침 중 오류가 발생했습니다. 나중에 다시 시도하세요.
미디어 소스 선택
- 상품 이름과 설명을 생성하는 동안 오류가 발생했습니다.
감지된 텍스트가 없습니다. 다른 포장 사진을 선택하거나 수동으로 상품 상세 정보를 입력하세요.
상품 추가
바코드 스캔
@@ -656,9 +707,6 @@ Language: ko_KR
직불카드 또는 신용카드로 %s 결제를 시도합니다.\n완료하면 결제가 환불됩니다.
쉽고 안전합니다.
모든 종류의 대면 결제를 \n스마트폰에서 허용합니다. 추가 하드웨어가 필요하지 않습니다.
- 예산
- 클릭 수
- 노출 수
거부됨
완료됨
활성
@@ -691,46 +739,23 @@ Language: ko_KR
설정
이메일을 사용하여 수동으로 상세 정보 추가
앱 로그인 요청을 처리하지 못했습니다.
- 클립보드에 상품 이름을 복사하는 중 오류가 발생했습니다.
- 상품 이름이 클립보드에 복사되었습니다.
- AI로 생성된 상품 이름
클립보드에 상품 설명을 복사하는 중 오류가 발생했습니다.
상품 설명이 클립보드에 복사되었습니다.
상품이 생성되지 않았습니다. 다시 시도하세요.
주소 정리 및 이 요율 사용 중지
이 주문에 대한 새 세율 설정
자동으로 세율 추가하기
- 상품 이름 생성 중 문제가 발생했습니다. 다시 시도하세요.
- 재생성
- 작성 의뢰
- 상품이 무엇이고 어떤 점이 독특한지 말씀해 주세요!
- AI에게 매력적인 제목 생성 맡기기
모든 상품 리뷰를 표시하려면 읽지 않음 필터를 비활성화해 보세요.
- 상품 이름
읽지 않은 상품 리뷰 없음
- 선택된 어조
설득력
화려
정중
평상시
- 브랜드에 어울리는 상품 프레젠테이션을 위해 어조와 목소리를 설정하세요.
어조 및 목소리
상세 정보
- 상품 설명
상품 이름
- 아래의 상세 정보는 나중에 언제든지 변경하실 수 있습니다.
미리보기
- 상품 상세 정보 생성
- 어조 및 목소리 설정
- 온라인에서 상품을 찾는 데 도움이 되도록 주요 기능, 이점 또는 상세 정보를 추가하세요.
예: 부드러운 원단, 튼튼한 바느질, 독특한 디자인
- 어떤 부분이 상품을 독특하게 만드는지 강조하고 AI가 만들어 내는 놀라운 결과물을 확인하세요.
- 상품 소개
- 이름 제안
- 예: 부드러운 원단, 튼튼한 바느질, 독특한 디자인
- 상품 이름
- 선택의 폭을 넓히려면 눌러서 더 많은 이름 제안을 볼 수 있습니다.
- 상품 이름 추가
AI를 활용했습니다. <a href=\'guidelines\'><u>자세히 알아보세요</u></a>.
수동으로 상품 및 상세 정보 추가
수동으로 추가
@@ -1054,7 +1079,6 @@ Language: ko_KR
특별 행사로 매출 증대
스토어 보기
최신 상태로 유지
- 모바일 결제에 참여
관리자에서 자세히 관리
일반
설정
diff --git a/WooCommerce/src/main/res/values-night/colors_base.xml b/WooCommerce/src/main/res/values-night/colors_base.xml
index 3b441e6c9d6..452dae09fa7 100644
--- a/WooCommerce/src/main/res/values-night/colors_base.xml
+++ b/WooCommerce/src/main/res/values-night/colors_base.xml
@@ -131,7 +131,6 @@
- @color/woo_white
@color/woo_white
@color/woo_purple_40
diff --git a/WooCommerce/src/main/res/values-nl/strings.xml b/WooCommerce/src/main/res/values-nl/strings.xml
index 5c1e71aa4db..5bd47686a17 100644
--- a/WooCommerce/src/main/res/values-nl/strings.xml
+++ b/WooCommerce/src/main/res/values-nl/strings.xml
@@ -1,11 +1,71 @@
+ Ongeldige sleutel: verwijder het teken \'_\' van het begin.
+ Deze sleutel wordt al gebruikt voor een ander aangepast veld.\nDe app ondersteunt momenteel niet het aanmaken van meerdere identieke sleutels. Gebruik wp-admin om een sleutel te kopiëren als dit nodig is.
+ Aangepaste velden toevoegen
+ Aangepast veld verwijderd
+ Het opslaan van de wijzigingen is mislukt, probeer het nog eens
+ Wijzigingen opgeslagen
+ Wijzigingen aan het opslaan
+ Het lijkt erop dat je niet bent verbonden met het internet. Zorg ervoor dat je wifi aan staat. Als je mobiele data gebruikt, zorg er dan voor dat deze is ingeschakeld in de instellingen van je apparaat.
+ Scan mislukt. Probeer het later opnieuw
+ Waarde
+ Sleutel
+ Andere producttypen, zoals variabel en virtueel, worden in toekomstige updates beschikbaar.
+ Alleen simpele fysieke producten zijn momenteel beschikbaar met POS.
+ Annuleren
+ Duur
+ De campagne loopt totdat je deze stopzet.
+ Stel de duur in
+ tot %1$s
+ Inplannen
+ Dagelijkse uitgaven
+ Hoeveel wil je uitgeven aan je campagne, en hoelang wil je dat deze loopt?
+ %1$s ➔ %2$s
+ Zorg dat je producten door miljoenen worden gezien met Blaze en geef je verkoop een boost
+ Wil je jouw verkoop een boost geven?
+ Fout bij het laden van aangepaste velden
+ Aangepaste velden
+ Achtergrond gedimd. Tik om het dialoogvenster te sluiten.
+ %1$s wekelijks
+ Uitvoeren tot ik dit zelf stopzet
+ Doorgaand vanaf %1$s
+ wekelijkse uitgaven
+ %1$s wekelijks vanaf %2$s
+ Wekelijks
+ Resterend
+ Totaal
+ Doorklikken
+ Het lijkt erop dat de energiebesparende modus op je apparaat aan staat. \nWe kunnen je winkelgegevens niet geven terwijl deze ingeschakeld is
+ Pop-upmenu met opties. Swipe om door de items te navigeren.
+ Taakbalkmenu openen
+ Taakbalk met status van kaartlezer. Het menu is geopend. Tik dubbel voor interacties.
+ Taakbalk met status van kaartlezer. Tik dubbel voor interacties.
+ Menu uitgeschakeld
+ Menu ingeschakeld
+ Kaartlezer niet verbonden. Tik dubbel om te verbinden
+ Kaartlezer verbonden
+ Achtergrond gedimd. Tik om het menu te sluiten.
+ Vinkje voor Betaling gelukt
+ Dit artikel uit winkelwagen verwijderen
+ Alle bestellingen die nog in behandeling zijn, zullen verloren gaan.
+ Verkooppuntmodus afsluiten?
+ Sluiten
+ Achtergrond gedimd. Tik om het dialoogvenster te sluiten.
+ Tik dubbel om het dialoogvenster te sluiten
+ Dialoogvenster voor Alleen simpele producten
+ Tik dubbel voor meer informatie
+ Banner voor Alleen simpele producten
+ Promoot je producten met Blaze Ads en verhoog nu je omzet.
+ Verhoog je omzet
+ Neem onderweg betalingen aan
+ Er is een onjuiste pincode ingevoerd. Probeer het opnieuw of probeer een andere betaalmethode
Menu
Product in winkelwagen %s, prijs %s
Product %s, prijs %s
@@ -14,12 +74,11 @@ Language: nl
Nieuwe bestelling
OK
+ Maak een bestelling aan in het winkelbeheer
- Verlaat POS en maak een nieuwe bestelling aan vanuit het tabblad Bestellingen om betalingen te ontvangen voor een niet-simpel product.
- Alleen simpele fysieke producten zijn momenteel beschikbaar met POS.\nAndere producttypen, zoals variabel en virtueel, worden in toekomstige updates beschikbaar.
+ Verlaat POS en maak een nieuwe bestelling aan vanuit het tabblad Bestellingen om betalingen te ontvangen voor een niet-simpel product.
Waarom kan ik mijn producten niet zien?
Informatie
Sluiten
- Meer informatie
+ Meer\u00A0informatie
Alleen simpele fysieke producten zijn momenteel compatibel met POS. Andere producttypen, zoals variabel en virtueel, worden in toekomstige updates beschikbaar.
Alleen simpele producten weergeven
Websiteadres
@@ -39,8 +98,6 @@ Language: nl
POS ondersteunt momenteel alleen simpele producten – \nmaak er een aan om te beginnen.
Geen ondersteunde producten gevonden
Geen producten
- Laten we wat klanten helpen
- Aan de slag
Vraag om ondersteuning
Verbind je lezer
Foto verwijderd
@@ -135,8 +192,7 @@ Language: nl
Geen regels voor hoeveelheid
Bezoekers
Annuleren
- Afsluiten
- Weet je zeker dat je POS wilt afsluiten?
+ Afsluiten
POS afsluiten
Verwijder %s uit winkelwagen
Afrekenen
@@ -419,20 +475,16 @@ Language: nl
Slogan
Afbeelding wijzigen
Toepassen
- Begint
+ Startdatum
%1$s dagen
- Tijdsduur instellen
Impressies reflecteren hoe vaak je advertentie door potentiële klanten te zien is.\n\n\n Door wisselend online verkeer en gebruikersgedrag kunnen er geen exacte getallen gegarandeerd worden maar we proberen er voor te zorgen dat de daadwerkelijke impressies van je advertentie zoveel mogelijk overeenkomen met het doelaantal.\n\n\n Vergeet niet dat het bij impressies draait om zichtbaarheid en niet om de acties die worden ondernomen door de mensen die het zien.
Gereed
Impressies
Updaten
Bewerken
- Duur
Geschatte aantal mensen dat per dag bereikt wordt
%1$s per dag
gedurende %1$s dagen
- Totaal uitgegeven
- Hoeveel wil je uitgeven aan de promotiecampagne voor je product?
Stel je budget in
Alles
%1$s dagen vanaf %2$s
@@ -634,7 +686,6 @@ Language: nl
Boost je verkoopcijfers met Blaze
Er is een fout opgetreden bij het vernieuwen van de campagnelijst. Probeer later opnieuw.
Mediabron selecteren
- Er is een fout opgetreden bij de generatie van een productnaam en -beschrijving.
Geen tekst gedetecteerd. Kies een andere foto van de verpakking of voer de productgegevens handmatig in.
Product toevoegen
Streepjescode scannen
@@ -656,9 +707,6 @@ Language: nl
Probeer een %s-betaling met je betaal- of creditcard.\nJe zal het terugbetaald krijgen zodra je klaar bent.
Het is eenvoudig, veilig en privé.
Accepteer alle typen fysieke betalingen, direct\nop je telefoon. Geen extra hardware nodig.
- Budget
- Klikken
- Impressies
Afgewezen
Voltooid
Actief
@@ -691,46 +739,23 @@ Language: nl
INSTELLINGEN
Details handmatig toevoegen met behulp van e-mail
We konden de loginaanvraag van je app niet verwerken
- Fout bij kopiëren naar klembord.
- Productnaam gekopieerd naar klembord.
- Productnaam gegenereerd door AI
Fout bij kopiëren productbeschrijving naar klembord.
Productbeschrijving gekopieerd naar klembord.
Productgeneratie mislukt. Probeer het nog eens
Verwijder het adres en stop met het gebruiken van dit tarief
Stel een nieuw belastingtarief voor deze bestelling
Automatisch het belastingtarief toevoegen
- Er is een probleem opgetreden tijdens het genereren van de productnaam. Probeer het opnieuw.
- Regenereren
- Schrijf het voor mij
- Vertel ons wat jouw product is en wat dit uniek maakt!
- Laat AI pakkende titels voor je genereren
- Productnaam
Probeer om het ‘ongelezen’ filter uit te schakelen om al je productbeoordelingen te bekijken
Geen ongelezen productbeoordelingen
- Toon geselecteerd
Overtuigend
Fleurig
Formeel
Casual
- Stel de toon in om de presentatie van je product af te stemmen op je merk.
Toon
Details
- Productbeschrijving
Productnaam
- Je kan de onderstaande gegevens later altijd wijzigen.
Voorbeeld
- Productgegevens aanmaken
- Toon instellen
- Voeg belangrijke functies, voordelen of details toe om je product beter online vindbaar te laten worden.
Bijvoorbeeld Zachte stof, duurzaam stikwerk, uniek ontwerp
- Markeer wat je product uniek maakt en laat AI het werk voor je doen.
- Over je product
- Stel een naam voor
- Bijvoorbeeld: Zachte stof, duurzaam stikwerk, uniek ontwerp.
- Productnaam
- Of breid je keuzes uit door te tikken voor meer naamvoorstellen.
- Voeg je productnaam toe
Mogelijk gemaakt door AI. <a href=\'guidelines\'><u>Meer informatie</u></a>.
Handmatig een product en de details toevoegen
Handmatig toevoegen
@@ -1054,7 +1079,6 @@ Language: nl
Geef je verkopen een boost met speciale aanbiedingen
Bekijk je winkel
Blijf up-to-date
- Start ook met mobiele betalingen
Beheer meer bij beheer
Algemeen
Instellingen
diff --git a/WooCommerce/src/main/res/values-pt-rBR/strings.xml b/WooCommerce/src/main/res/values-pt-rBR/strings.xml
index 281f6c4bf18..0a564ca41d4 100644
--- a/WooCommerce/src/main/res/values-pt-rBR/strings.xml
+++ b/WooCommerce/src/main/res/values-pt-rBR/strings.xml
@@ -1,11 +1,71 @@
+ Chave inválida: remova o caractere \"_\" do início.
+ Esta chave já está sendo utilizada por outro campo personalizado.\nO aplicativo atual não aceita criação de chaves duplicadas. Se necessário, use o Painel WP Admin para duplicar uma chave.
+ Adicionar campos personalizados
+ Campo personalizado excluído
+ Falha ao salvar alterações, tente novamente
+ Alterações salvas
+ Salvando alterações
+ Falha na verificação. Tente novamente mais tarde
+ Parece que você não está conectado à Internet. Verifique se seu Wi-Fi está ligado. Se você estiver usando dados móveis, verifique se a opção está ativada nas configurações do seu dispositivo.
+ Valor
+ Chave
+ Outros tipos de produtos, como variáveis e virtuais, serão disponibilizados em atualizações futuras.
+ Agora apenas produtos físicos simples podem ser usados nos pontos de venda.
+ Cancelar
+ Duração
+ A campanha continuará ativa até você interrompê-la.
+ Especificar a duração
+ até %1$s
+ Programação
+ Gasto diário
+ Quanto você gostaria de gastar na sua campanha e por quanto tempo quer mantê-la ativa?
+ %1$s ➔ %2$s
+ Exiba seus produtos para milhões de pessoas com o Blaze e aumente as vendas
+ Quer aumentar suas vendas?
+ Erro ao carregar campos personalizados
+ Campos personalizados
+ Plano de fundo esmaecido. Toque para ignorar a caixa de diálogo.
+ %1$s por semana
+ Executar até que eu interrompa
+ %1$s por semana começando em %2$s
+ Semanal
+ Restante
+ Total
+ Taxa de cliques
+ Parece que seu dispositivo está no modo de economia de bateria. \nNão é possível ver as informações da loja enquanto ele está ativo.
+ De %1$s em diante
+ gasto semanal
+ Menu pop-up com opções. Deslize para navegar pelos itens.
+ Menu desativado
+ Menu ativado
+ O leitor de cartão não está conectado. Toque duas vezes para conectar
+ Abra menu da barra de ferramentas
+ Barra de ferramentas com status do leitor do cartão. O menu está aberto. Toque duas vezes para interagir.
+ Barra de ferramentas com status do leitor do cartão. Toque duas vezes para interagir.
+ O leitor de cartão está conectado
+ Plano de fundo escurecido. Toque para fechar o menu.
+ Ícone de pagamento concluído
+ Remova esse item do carrinho
+ Pedidos em andamento serão perdidos.
+ Sair do modo de ponto de venda?
+ Plano de fundo escurecido. Toque para ignorar a caixa de diálogo.
+ Toque duas vezes para ignorar a caixa de diálogo
+ Caixa de diálogo apenas de produtos simples
+ Banner apenas de produtos simples
+ Impulsione suas vendas
+ Feche
+ Toque duas vezes para saber mais
+ Promova seus produtos e aumente as vendas agora mesmo com os anúncios do Blaze.
+ Aceite pagamentos onde estiver
+ Um PIN incorreto foi inserido. Tente novamente ou use outro meio de pagamento
Menu
Produto no carrinho %s. Preço: %s
Produto %s. Preço: %s
@@ -14,14 +74,13 @@ Language: pt_BR
Novo pedido
OK
+ Criar um pedido no gerenciamento da loja
- Para processar o pagamento de um produto que não é simples, saia do ponto de venda e faça um novo pedido na guia de pedidos.
- Agora apenas produtos físicos simples podem ser usados nos pontos de venda.\nOutros tipos de produtos, como variáveis e virtuais, serão disponibilizados em atualizações futuras.
Por que não consigo ver meus produtos?
Informação
Fechar
- Saiba mais
Agora apenas produtos físicos simples são compatíveis nos pontos de venda. Outros tipos de produtos, como variáveis e virtuais, serão disponibilizados em atualizações futuras.
Mostrando apenas produtos simples
+ Para processar o pagamento de um produto que não é simples, saia do ponto de venda e faça um novo pedido na guia de pedidos.
+ Saiba\u00A0mais
Endereço do site
Google para WooCommerce
Adicionar campanha paga
@@ -39,12 +98,10 @@ Language: pt_BR
Tentar novamente
Nenhum produto
Nenhum produto compatível encontrado
- Vamos atender a alguns clientes
Obter suporte
Conecte o seu leitor
Imagem removida
Escaneando imagem
- Iniciando
Texto da imagem adicionado às informações iniciais
Cliques
Impressões
@@ -135,11 +192,10 @@ Language: pt_BR
Impostos
Público
Cancelar
- Sair
- Tem certeza de que deseja sair do PDV?
Sair do PDV
Finalizar compra
Remover %s do carrinho
+ Sair
Status do leitor desconhecido
Checkout
Leitor conectado
@@ -418,24 +474,20 @@ Language: pt_BR
Descrição
Mudar imagem
Aplicar
- Início
%1$s dias
- Definir duração
As impressões demonstram a frequência com que seu anúncio aparece para possíveis clientes.\n\n\n Apesar de o número exato não poder ser garantido por conta das variações no tráfego online e no comportamento do usuário, nós nos esforçamos para corresponder ao máximo as impressões reais do seu anúncio à contagem desejada.\n\n\n E lembre-se: as impressões têm a ver com a visibilidade, e não com as ações dos visitantes.
Concluído
Impressões
Atualizar
Editar
- Duração
Estimativa de pessoas alcançadas por dia
%1$s ao dia
por %1$s dias
- Total gasto
- Quanto você gostaria de gastar na campanha de divulgação do seu produto?
Defina seu orçamento
Tudo
%1$s dias a partir de %2$s
Detalhamento
+ Data inicial
Não mostrar novamente
Lembrar depois
Tem um minuto? Ajude a melhorar nossas funcionalidades assistidas por IA dando um feedback rápido.
@@ -634,7 +686,6 @@ Language: pt_BR
Impulsione as vendas na sua loja com o Blaze
Ocorreu um erro ao atualizar a lista de campanhas. Tente novamente mais tarde.
Selecionar fonte de mídia
- Ocorreu um erro ao gerar o nome e a descrição do produto.
Nenhum texto encontrado. Selecione outra foto de embalagem ou insira os detalhes do produto manualmente.
Adicionar produto
Escanear código de barras
@@ -656,9 +707,6 @@ Language: pt_BR
Experimente um pagamento no valor de %s com seu cartão de crédito ou débito.\nEle será reembolsado quando você concluir.
É fácil, seguro e confidencial.
Aceite todos os tipos de pagamentos presenciais, direto\nno seu telefone. Não é necessário hardware adicional.
- Orçamento
- Cliques
- Impressões
Rejeitada
Concluída
Ativa
@@ -691,45 +739,23 @@ Language: pt_BR
CONFIGURAÇÕES
Adicionar detalhes manualmente usando o e-mail
Não foi possível processar sua solicitação de login no app
- Erro ao copiar o nome do produto para a área de transferência.
- Nome do produto copiado para a área de transferência.
- Nome do produto gerado por IA
Erro ao copiar a descrição do produto para a área de transferência.
Descrição do produto copiada para a área de transferência.
Falha ao gerar o produto. Tente novamente
Apagar endereço e parar de usar esta tarifa
Definir um novo imposto para este pedido
Inclusão automática de imposto
- Ocorreu um problema ao gerar o nome do produto. Tente novamente.
- Gerar novamente
- Escreva por mim
- Conte para nós qual o seu produto e o que o torna único.
- Deixe a IA gerar títulos cativantes para você
Tente desativar o filtro de não lidas para ver todas as avaliações de produto
- Nome do produto
Nenhuma avaliação de produto não lida
- Tom selecionado
Convincente
Rebuscado
Formal
Casual
- Defina o tom e a voz para moldar a apresentação do seu produto de forma que reflita a identidade da sua marca.
Tom e voz
Detalhes
- Descrição do produto
Nome do produto
- Você pode alterar os detalhes abaixo a qualquer momento.
Visualização
- Criar detalhes do produto
- Definir tom e voz
- Adicione as principais funcionalidades, benefícios ou detalhes para que seu produto seja encontrado facilmente online.
Por exemplo, tecido macio, costura resistente, design único
- Destaque o diferencial do seu produto e espere a magia acontecer com a IA.
- Sobre o seu produto
- Sugerir um nome
- Nome do produto
- Se quiser outras opções, toque para ver mais sugestões de nome.
- Adicionar o nome do produto
Com tecnologia de IA. <a href=\'guidelines\'><u>Saiba mais.</u></a>
Adicione um produto e os detalhes manualmente
Adicionar manualmente
@@ -738,7 +764,6 @@ Language: pt_BR
Adicionar um produto
Apenas avaliações não lidas
Editar configuração de imposto
- Por exemplo: tecido macio, costura resistente, design único.
Isso não afetará pedidos online
Adicionar esta tarifa a todos os pedidos criados
Editar impostos
@@ -1063,7 +1088,6 @@ Language: pt_BR
Produtos agrupados
Não agrupados
Pacote
- Habilite os pagamentos por dispositivo móvel
Sem máximo
Sem mínimo
Grupo de
diff --git a/WooCommerce/src/main/res/values-ru/strings.xml b/WooCommerce/src/main/res/values-ru/strings.xml
index 02978a3b4a2..c139c1745b3 100644
--- a/WooCommerce/src/main/res/values-ru/strings.xml
+++ b/WooCommerce/src/main/res/values-ru/strings.xml
@@ -1,11 +1,71 @@
+ Недопустимый ключ: удалите символ «_» в самом начале.
+ Этот ключ уже применяется в другом произвольном поле.\nВ настоящий момент приложение не поддерживает создание дубликатов ключей. При необходимости создать дубликат ключа воспользуйтесь wp-admin.
+ Добавить произвольные поля
+ Произвольное поле удалено
+ Не удалось сохранить изменения. Повторите попытку
+ Изменения сохранены
+ Сохранение изменений
+ Похоже, отсутствует подключение к Интернету. Убедитесь, что ваш Wi-Fi включён. Если вы используете мобильные данные, убедитесь, что они включены в настройках вашего устройства.
+ Сбой сканирования. Повторите попытку позже
+ Значение
+ Ключ
+ Другие типы товаров, в частности виртуальные и вариативные товары, станут доступны в ближайших обновлениях.
+ В данный момент в режиме POS поддерживаются только простые материальные товары.
+ Отмена
+ Продолжительность
+ Кампания будет идти до тех пор, пока вы её не остановите.
+ Указать продолжительность
+ по %1$s
+ Расписание
+ Ежедневные затраты
+ Сколько вы планируете потратить на кампанию и сколько времени она должна продлиться?
+ %1$s ➔ %2$s
+ При помощи Blaze демонстрируйте ваши товары миллионам потенциальных покупателей и повышайте продажи
+ Думаете о том, как повысить продажи?
+ Ошибка при загрузке произвольных полей
+ Произвольные поля
+ Затемнённый фон. Коснитесь, чтобы закрыть диалог.
+ %1$s в неделю
+ Выполнять до остановки мною
+ Запущена %1$s
+ еженедельные расходы
+ %1$s еженедельно начиная с %2$s
+ Еженедельно
+ Осталось
+ Итого
+ Переходы
+ По-видимому, ваше устройство находится в режиме экономии энергии. \nПока этот режим активен, сведения о магазине будут недоступны
+ Всплывающее меню с опциями. Смахивайте товары, чтобы переходить к следующим.
+ Открыть меню панели инструментов
+ Панель инструментов со статусом платёжного терминала. Меню открыто. Дважды коснитесь, чтобы начать работу.
+ Панель инструментов со статусом платёжного терминала. Дважды коснитесь, чтобы начать работу.
+ Меню отключено
+ Меню включено
+ Платёжный терминал не подключён. Дважды коснитесь, чтобы подключить
+ Платёжный терминал подключён
+ Затемнённый фон. Коснитесь, чтобы закрыть меню.
+ Значок «Оплачено» в виде флажка
+ Удалить товар из корзины
+ Все заказы, которые находятся в процессе выполнения, будут потеряны.
+ Выйти из режима «Пункт продажи»?
+ Закрыть
+ Затемнённый фон. Коснитесь, чтобы закрыть диалог.
+ Дважды коснитесь, чтобы закрыть диалог.
+ Диалог «Только простые товары»
+ Коснитесь, чтобы узнать подробнее.
+ Баннер «Только простые товары»
+ Продвигайте товары при помощи Blaze Ads и повышайте продажи.
+ Повышайте продажи
+ Принимайте платежи на ходу
+ Введён неверный PIN-код. Повторите попытку или попробуйте другое средство платежа
Меню
Товар в корзине %s, цена %s
Товар %s, цена %s
@@ -14,8 +74,7 @@ Language: ru
Новый заказ
ОК
+ Создать заказ в разделе «Управление магазином»
- Чтобы принять платёж за товар, не относящийся к простым, выйдите из POS и создайте новый заказ в таблице заказов.
- В данный момент POS поддерживает только простые материальные товары.\nДругие типы товаров, в частности виртуальные и вариативные товары, станут доступны в ближайших обновлениях.
+ Чтобы принять платёж за товар, не относящийся к простым, выйдите из режима POS и создайте новый заказ в таблице заказов.
Почему я не вижу свои товары?
Информация
Закрыть
@@ -39,8 +98,6 @@ Language: ru
В данный момент POS поддерживает только простые товары — \nсначала создайте такой товар.
Нет поддерживаемых товаров
Товаров нет
- Давайте обслужим нескольких клиентов
- Начало работы
Поддержка
Подключите платёжный терминал
Фотография удалена
@@ -135,8 +192,7 @@ Language: ru
Нет правил количества
Аудитория
Отмена
- Выход
- Вы уверены, что хотите закрыть POS?
+ Выход
Закрыть POS
Удалить %s из корзины
Оформление заказа
@@ -419,20 +475,16 @@ Language: ru
Ключевая фраза
Изменить изображение
Применить
- Начало
+ Дата начала
%1$s дн.
- Задать продолжительность
Раздел «Показы» отражает частоту, с которой ваша реклама появляется на экранах потенциальных клиентов.\n\n\n Достичь этой цифры в точности будет невозможно из-за колебаний посещаемости и различного поведения пользователей, однако мы стремимся к тому, чтобы реальное число показов рекламы максимально приближалось к целевому показателю.\n\n\n Учитывайте, что показы влияют лишь на видимость рекламы, а не на действия читателей.
Готово
Показы
Обновить
Изменить
- Продолжительность
Приблизительный ежедневный охват пользователей
%1$s ежедневно
на %1$s дн.
- Общие расходы
- Сколько вы хотите потратить на кампанию по продвижению товара?
Настройте бюджет
Все
%1$s дн. с %2$s
@@ -634,7 +686,6 @@ Language: ru
Повышайте продажи в своём магазине с помощью Blaze
При обновлении списка кампаний произошла ошибка. Повторите попытку позже.
Выберите источник медиафайлов
- При создании названия и описания товара произошла ошибка.
Текст не обнаружен. Выберите другую фотографию упаковки или введите сведения о товаре вручную.
Добавить товар
Сканировать штрихкод
@@ -656,9 +707,6 @@ Language: ru
Попробуйте оплатить %s банковской картой.\nПосле этого средства будут возвращены.
Просто, безопасно и конфиденциально.
Принимайте все виды очных платежей прямо\nна вашем телефоне. Дополнительное оборудование не требуется.
- Бюджет
- Нажатий
- Показов
Отклонено
Завершено
Активно
@@ -691,46 +739,23 @@ Language: ru
НАСТРОЙКИ
Добавить сведения вручную, используя электронный адрес
Не удалось обработать запрос на вход в приложение
- Ошибка при копировании названия товара в буфер обмена
- Название товара скопировано в буфер обмена.
- Название товара создано при помощи ИИ
Ошибка при копировании описания товара в буфер обмена.
Описание товара скопировано в буфер обмена.
При создании товара возникла ошибка. Повторите попытку
Очистить адрес и больше не использовать эту ставку
Установить другую налоговую ставку для этого заказа
Автоматическое добавление налоговой ставки
- Возникла проблема при создании названия товара. Повторите попытку.
- Создать повторно
- Напишите это за меня
- Расскажите о своём товаре и его отличительных качествах!
- С помощью ИИ вы можете создавать привлекательные заголовки
- Название товара
Попробуйте отключить фильтр «Непрочитанные», чтобы просмотреть все отзывы о товарах
Нет непрочитанных отзывов о товарах
- Выбранный тон
Убедительный
Цветистый
Строгий
Непринужденный
- Задайте тон и стиль презентации, чтобы она соответствовала характеру вашего бренда.
Тон и стиль
Сведения
- Описание товара
Название товара
- Приведённые ниже сведения можно будет отредактировать в любой момент.
Предварительный просмотр
- Создание сведений о товаре
- Задайте тон и стиль
- Добавьте ключевые характеристики, преимущества и подробные сведения, чтобы покупателям проще было найти ваш товар в Интернете.
Например: мягкая ткань, прочные швы, уникальный дизайн
- Укажите отличительные качества вашего товара, и ИИ сделает за вас всё остальное.
- Информация о товаре
- Предложите название товара
- Например: мягкая ткань, прочные швы, уникальный дизайн
- Название товара
- Можно также расширить выбор: нажмите, чтобы увидеть больше вариантов.
- Добавить название товара
На основе ИИ. <a href=\'guidelines\'><u>Подробнее</u></a>.
Добавьте товар и сведения вручную
Добавить вручную
@@ -1054,7 +1079,6 @@ Language: ru
Увеличивайте продажи с помощью специальных предложений
Просматривайте магазин
Следите за новостями
- Подключите мобильные платежи
Управляйте магазином через консоль
Общее
Настройки
diff --git a/WooCommerce/src/main/res/values-sv/strings.xml b/WooCommerce/src/main/res/values-sv/strings.xml
index 2f669be4973..faac13ff50a 100644
--- a/WooCommerce/src/main/res/values-sv/strings.xml
+++ b/WooCommerce/src/main/res/values-sv/strings.xml
@@ -1,11 +1,71 @@
+ Ogiltig nyckel: ta bort tecknet \"_\" från början.
+ Nyckeln används redan för ett annat anpassat fält.\nAppen har för närvarande inte stöd för att skapa dubbletter av nycklar. Använd WP-admin för att duplicera en nyckel om det behövs.
+ Lägg till anpassade fält
+ Anpassat fält borttaget
+ Skanning misslyckades. Försök igen senare
+ Ändringar sparade
+ Sparar ändringar
+ Misslyckades att spara ändringar, försök igen
+ Det verkar som att du inte är ansluten till internet. Kontrollera att ditt Wi-Fi är på. Se till att mobildata är aktiverat i dina enhetsinställningar om du använder detta.
+ Värde
+ Nyckel
+ Andra produkttyper, till exempel rörliga och virtuella, kommer att bli tillgängliga i framtida uppdateringar.
+ Avbryt
+ Varaktighet
+ Kampanj kommer köras tills du stoppar den.
+ Specificera varaktigheten
+ Endast enkla fysiska produkter kan användas med POS just nu.
+ till %1$s
+ Schema
+ Dagligt belopp
+ %1$s ➔ %2$s
+ Hur mycket vill du spendera på din kampanj, och hur länge ska den köras?
+ Visa upp dina produkter för miljontals människor med Blaze och öka din försäljning
+ Funderar du på hur du kan öka din försäljning?
+ Det gick inte att läsa in anpassade fält
+ Anpassade fält
+ Nedtonad bakgrund. Tryck för att avfärda dialogrutan.
+ %1$s per vecka
+ Kör tills jag stoppar den
+ %1$s per vecka, från och med %2$s
+ Per vecka
+ Återstående
+ Totalt
+ Klick
+ Det verkar som att din enhet är i strömsparläge. \nVI kan inte tillhandahålla din butiksinformation medan det är aktiverat
+ Pågående från %1$s
+ veckobelopp
+ Meny inaktiverad
+ Meny aktiverad
+ Kortläsare ansluten
+ Kortläsare inte ansluten. Dubbeltryck för att ansluta
+ Popup-meny med alternativ. Svep för att navigera bland objekt.
+ Öppna verktygsfältsmeny
+ Verktygsfält med kortläsarstatus. Dubbeltryck för att interagera.
+ Verktygsfält med kortläsarstatus. Menyn är öppen. Dubbeltryck för att interagera.
+ Nedtonad bakgrund. Tryck för att stänga menyn.
+ Bockmarkeringsikon för lyckad betalning
+ Ta bort den här varan från varukorgen
+ Dubbeltryck för att lära dig mer
+ Stäng
+ Öka din försäljning
+ Eventuella pågående beställningar kommer att gå förlorade.
+ Lämna Försäljningsplatsläge?
+ Nedtonad bakgrund. Tryck för att avfärda dialogrutan.
+ Dubbeltryck för att avfärda dialogrutan
+ Dialogruta – endast enkla produkter
+ Banner – endast enkla produkter
+ Marknadsför dina produkter med Blaze-annonser och öka din försäljning nu.
+ Ta emot betalningar i farten
+ En felaktig PIN-kod har angetts. Försök igen eller använd en annan betalningsmetod
Meny
Produkt i varukorg %s, pris %s
Produkt %s, pris %s
@@ -17,11 +77,10 @@ Language: sv_SE
Varför kan jag inte se mina produkter?
Info
Stäng
- Lär dig mer
- För att ta betalt för en icke-enkel produkt, lämna POS och skapa en ny beställning från fliken Beställningar.
- Endast enkla fysiska produkter kan användas med POS just nu.\nAndra produkttyper, till exempel rörliga och virtuella, kommer att bli tillgängliga i framtida uppdateringar.
Endast enkla fysiska produkter är kompatibla med POS just nu. Andra produkttyper, till exempel rörliga och virtuella, kommer att bli tillgängliga i framtida uppdateringar.
Visar endast enkla produkter
+ För att ta betalt för en icke-enkel produkt, lämna POS och skapa en ny beställning från fliken Beställningar.
+ Läs mer
Webbplatsadress
Lägg till betald kampanj
Google för WooCommerce
@@ -38,9 +97,7 @@ Language: sv_SE
POS stöder för närvarande bara enkla produkter
POS stöder för närvarande bara enkla produkter – \nskapa en för att komma igång.
Inga produkter
- Låt oss betjäna några kunder
Inga produkter som stöds hittades
- Startar upp
Skaffa support
Anslut din läsare
Foto borttaget
@@ -135,8 +192,7 @@ Language: sv_SE
Momser
Målgrupp
Avbryt
- Avsluta
- Är du säker på att du vill avsluta POS?
+ Avsluta
Avsluta POS
Kassa
Ta bort %s från varukorg
@@ -418,24 +474,20 @@ Language: sv_SE
Ändra bild
Tillämpa
%1$s dagar
- Ställ in varaktighet
Klar
Uppdatera
Redigera
- Varaktighet
Ställ in din budget
i %1$s dagar
- Totalt spenderat
%1$s dagar från %2$s
Alla
%d tecken återstår
- Startar
%1$s dagligen
- Hur mycket skulle du vilja spendera på din produktmarknadsföringskampanj?
Föreslaget av AI
+ Visningar återspeglar hur ofta din annons visas för potentiella kunder.\n\n\n Exakta siffror kan inte garanteras på grund av fluktuerande onlinetrafik och användarbeteende, men vi strävar efter att annonsens faktiska antal visningar ska ligga så nära ditt målantal som möjligt.\n\n\n Kom ihåg att visningar handlar om synlighet, inte om åtgärder som vidtas av tittarna.
Visningar
Uppskattat antal personer som nås per dag
- Visningar återspeglar hur ofta din annons visas för potentiella kunder.\n\n\n Exakta siffror kan inte garanteras på grund av fluktuerande onlinetrafik och användarbeteende, men vi strävar efter att annonsens faktiska antal visningar ska ligga så nära ditt målantal som möjligt.\n\n\n Kom ihåg att visningar handlar om synlighet, inte om åtgärder som vidtas av tittarna.
+ Startdatum
Visa det inte igen
Påminn mig senare
Har du tid en minut? Hjälp oss att förbättra våra AI-assisterade funktioner genom lite snabb feedback.
@@ -636,7 +688,6 @@ Language: sv_SE
Få mer försäljning i din butik med Blaze
Marknadsför din produkt på bara några minuter.
Välj mediekälla
- Ett fel uppstod vid generering av produktnamn och beskrivning.
Ingen text upptäckt. Välj ett annat förpackningsfoto eller ange produktinformation manuellt.
Lägg till produkt
Det uppstod ett fel vid uppdateringen av listan över kampanjer. Försök igen senare.
@@ -656,12 +707,9 @@ Language: sv_SE
Prova att genomföra en %s-betalning med ditt betal- eller kreditkort.\nBetalningen kommer att återbetalas när du är klar.
Det är enkelt, säkert och privat.
Ta emot alla typer av personliga betalningar, direkt\ni din telefon. Ingen extra hårdvara behövs.
- Budget
- Klick
Aktiv
Under granskning
Skapa kampanj
- Visningar
Avvisad
Har slutförts
Öka synligheten och få dina produkter sålda snabbt.
@@ -691,52 +739,29 @@ Language: sv_SE
INSTÄLLNINGAR
Lägg till uppgifter manuellt via e-post
Vi kunde inte behandla din begäran om inloggning i appen
- Det gick inte att kopiera produktnamnet till urklipp.
- Produktnamn kopierat till urklipp.
- Produktnamn genererat av AI
Det gick inte att kopiera produktbeskrivningen till urklipp.
Produktbeskrivning kopierad till urklipp.
Produktgenereringen misslyckades Försök igen
- Återskapa
- Skriv det åt mig
- Berätta vad din produkt är och vad som gör den unik!
- Låt AI generera fängslande rubriker åt dig
Rensa adressen och sluta använda den här momssatsen
Ange en ny momssats för den här beställningen
Lägg till momssats automatiskt
- Det uppstod ett problem när produktnamnet skulle genereras. Försök igen.
Prova att inaktivera filtret för olästa produktrecensioner för att se alla dina produktrecensioner
- Produktnamn
Övertygande
Formell
Detaljer
- Produktbeskrivning
Produktnamn
Inga olästa produktrecensioner
- Ton vald
Blommig
Avslappnad
- Ställ in ton och röst för att forma presentationen av din produkt så att den passar ditt varumärke.
Ton och röst
- Du kan alltid ändra detaljerna nedan senare.
Förhandsgranska
- Skapa produktdetaljer
- Ställ in ton och röst
- Lägg till viktiga funktioner, fördelar eller information som hjälper kunder att hitta din produkt online.
Exempelvis mjukt tyg, slitstarka sömmar, unik design
- Om din produkt
- Föreslå ett namn
- Produktnamn
- Lägg till ditt produktnamn
Drivs med AI. <a href=\'guidelines\'><u>Lär dig mer</u></a>.
Lägg till en produkt och detaljerna manuellt
Lägg till manuellt
Skapa en produkt med AI
Lägg till en produkt
Endast olästa recensioner
- Framhäv vad som gör din produkt unik och låt AI utöva sin magi.
- Exempelvis mjukt tyg, slitstarka sömmar, unik design.
- Alternativt kan du utöka dina alternativ genom att trycka för fler namnförslag.
Snabbgenerera information åt dig
Redigera momssatsinställning
Betalningsmetoder
@@ -1059,7 +1084,6 @@ Language: sv_SE
%d produkter
1 produkt
Paket
- Skaffa mobila betalningsmöjligheter
Hantera mer på admin
Du kan redigera paketprodukter i webbadminpanelen.
Paketprodukter
diff --git a/WooCommerce/src/main/res/values-tr/strings.xml b/WooCommerce/src/main/res/values-tr/strings.xml
index d2991db13c0..facd36b99b7 100644
--- a/WooCommerce/src/main/res/values-tr/strings.xml
+++ b/WooCommerce/src/main/res/values-tr/strings.xml
@@ -1,11 +1,71 @@
+ Geçersiz anahtar: Lütfen en baştaki \"_\" karakterini kaldırın.
+ Bu anahtar zaten başka bir özel alan için kullanılıyor.\nUygulama şu anda yinelenen anahtar oluşturmayı desteklemiyor. Lütfen gerekirse bir anahtarı yinelemek için wp yöneticisini kullanın.
+ Özel alanlar ekle
+ Özel Alan silindi
+ Değişiklikler kaydedilemedi, lütfen yeniden deneyin
+ Değişiklikler kaydedildi
+ Değişiklikler kaydediliyor
+ Görünüşe göre internete bağlı değilsiniz. Wi-Fi ağınızın açık olduğundan emin olun. Mobil veri kullanıyorsanız cihazınızın ayarlarında etkinleştirildiğinden emin olun.
+ Tarama başarısız. Lütfen daha sonra tekrar deneyin
+ Değer
+ Anahtar
+ Değişken ve sanal gibi diğer ürün türleri gelecekteki güncellemelerde kullanılabilir olacak.
+ Şu anda POS ile yalnızca basit fiziksel ürünler kullanılabiliyor.
+ İptal Et
+ Süre
+ Kampanya siz durdurana kadar çalışır.
+ Süreyi belirle
+ %1$s öğesine
+ Zamanla
+ Günlük harcama
+ Kampanyanızda ne kadar harcama yapmak istersiniz ve bu ne kadar süre çalışmalı?
+ %1$s ➔ %2$s
+ Blaze sayesinde ürünlerinizin milyonlarca kişi tarafından görülmesini sağlayın ve satışlarınızı artırın
+ Satışlarınızı artırmayı mı düşünüyorsunuz?
+ Özel alanlar yüklenirken hata oluştu
+ Özel Alanlar
+ Soluk arkaplan. Pencereyi kapatmak için dokunun.
+ %1$s haftalık
+ Ben durdurana kadar çalıştır
+ %1$s tarihinden itibaren devam eden
+ haftalık harcama
+ %2$s tarihinden itibaren haftalık %1$s
+ Haftalık
+ Kalan
+ Toplam
+ Tıklanmalar
+ Görünüşe göre cihazınız Pil Koruyucu modunda. \nBu etkinken mağaza bilgilerinizi sağlayamıyoruz
+ Seçenekleri olan açılır pencere menüsü. Öğeler arasında gezinmek için kaydırın.
+ Araç çubuğu menüsünü aç
+ Kart okuyucu durumu olan araç çubuğu. Menü açık. Etkileşime girmek için iki kez dokunun.
+ Kart okuyucu durumu olan araç çubuğu. Etkileşime girmek için iki kez dokunun.
+ Menü Devre Dışı Bırakıldı
+ Menü Etkinleştirildi
+ Kart okuyucu bağlanmadı. Bağlanmak için çift dokunun
+ Kart okuyucu bağlandı
+ Soluk arkaplan. Menüyü kapatmak için dokunun.
+ Ödeme başarılı onay işareti simgesi
+ Bu ürünü sepetten çıkarın
+ Devam eden tüm siparişler kaybolacak.
+ Satış Noktası Modundan Çıkılsın Mı?
+ Kapat
+ Soluk arkaplan. Pencereyi kapatmak için dokunun.
+ Pencereyi kapatmak için iki kez dokunun
+ Yalnızca basit ürünler penceresi
+ Daha fazla bilgi edinmek için iki kez dokunun
+ Yalnızca basit ürünler başlığı
+ Blaze Reklamları ile hemen ürünlerinizi tanıtın ve satışlarınızı artırın.
+ Satışlarınızı güçlendirin
+ Hareket halindeyken ödeme alın
+ Yanlış PIN girildi. Yeniden deneyin veya başka bir ödeme yöntemi kullanın
Menü
Sepetteki ürün %s, Fiyatı %s
Ürün %s, Fiyatı %s
@@ -14,12 +74,11 @@ Language: tr
Yeni sipariş
Tamam
+ Mağaza yönetiminde sipariş oluşturun
- Temel olmayan bir ürünün ödemesini almak için POS\'tan çıkın ve siparişler sekmesinden yeni sipariş oluşturun.
- Şu anda POS ile yalnızca basit fiziksel ürünler kullanılabiliyor.\nDeğişken ve sanal gibi diğer ürün türleri gelecekteki güncellemelerde kullanılabilir olacak.
+ Temel olmayan bir ürünün ödemesini almak için POS\'tan çıkın ve siparişler sekmesinden yeni sipariş oluşturun.
Ürünlerimi neden göremiyorum?
Bilgi
Kapat
- Daha fazla bilgi edinin
+ Daha fazla\u00A0bilgi edinin
Şu anda POS ile yalnızca basit fiziksel ürünler uyumlu. Değişken ve sanal gibi diğer ürün türleri gelecekteki güncellemelerde kullanılabilir olacak.
Yalnızca basit ürünler gösteriliyor
Site Adresi
@@ -39,8 +98,6 @@ Language: tr
POS şu anda yalnızca basit ürünleri destekliyor, \nbaşlamak için bir tane oluşturun.
Desteklenen ürün bulunamadı
Ürün yok
- Birkaç müşteriye hizmet edelim
- Başlatılıyor
Destek Alın
Okuyucunuzu bağlayın
Fotoğraf kaldırıldı
@@ -135,8 +192,7 @@ Language: tr
Miktar Kuralı Yok
Hedef Kitle
İptal Et
- Çıkış
- POS\'tan çıkmak istediğinizden emin misiniz?
+ Çık
POS\'tan çık
%s ürününü sepetten çıkar
Ödeme
@@ -419,20 +475,16 @@ Language: tr
Site sloganı
Görseli değiştir
Uygula
- Başlangıç Tarihi
+ Başlangıç tarihi
%1$s gün
- Süre ayarlayın
İzlenimler, reklamlarınızın olası müşterilere gözükme sıklığını yansıtır.\n\n\n Dalgalı çevrimiçi trafik ve kullanıcı davranışından dolayı kesin sayılardan emin olunamasa da reklamınızın asıl izlenimlerini hedef sayıya olabildiğince yakın bir şekilde eşleştirmeye çalışıyoruz.\n\n\n İzlenimlerin görüntüleyiciler tarafından gerçekleştirilen işlemler yerine görünürlük hakkında olduğunu unutmayın.
Tamam
İzlenimler
Güncelle
Düzenle
- Süre
Gün başına ulaşılan tahmini insan sayısı
%1$s günlük
%1$s gün için
- Toplam harcama
- Ürün tanıtım kampanyanıza ne kadar harcamak istiyorsunuz?
Bütçenizi oluşturun
Tümü
%2$s tarihinden itibaren %1$s gün
@@ -634,7 +686,6 @@ Language: tr
Blaze ile mağazanızda daha fazla satış yapın
Kampanya listesi yenilenirken bir hata oluştu. Lütfen daha sonra tekrar deneyin.
Ortam Kaynağı Seçin
- Ürün adı ve açıklaması oluşturulurken bir hata oluştu.
Metin algılanmadı. Lütfen başka bir ambalaj fotoğrafı seçin veya ürün ayrıntılarını manuel olarak girin.
Ürün ekleyin
Barkodu tarayın
@@ -656,9 +707,6 @@ Language: tr
Banka veya kredi kartınızla bir %s ödemesi yapmayı deneyin.\nÖdeme, işiniz bittiğinde iade edilir.
Kolay, güvenli ve gizlidir.
Doğrudan telefonunuzdan her türde şahsen ödemeyi\nkabul edin. Ekstra donanım gerektirmez.
- Bütçe
- Tıklamalar
- İzlenimler
Reddedildi
Tamamlandı
Etkin
@@ -691,46 +739,23 @@ Language: tr
AYARLAR
Ayrıntıları e-posta kullanarak el ile ekle
Uygulamada oturum açma isteğinizi işleme koyamadık
- Ürün adını panoya kopyalamada hata.
- Ürün adı panoya kopyalandı.
- Ürün adı Yapay Zeka tarafından oluşturuldu
Ürün açıklamasını panoya kopyalamada hata.
Ürün açıklaması panoya kopyalandı.
Ürün oluşturma başarısız. Lütfen tekrar deneyin
Adresi temizleyin ve bu oranı kullanmayı bırakın
Bu sipariş için yeni bir vergi oranı belirleyin
Vergi oranını otomatik olarak ekleme
- Ürün adı oluşturulurken bir sorun oluştu. Lütfen tekrar deneyin.
- Yeniden oluştur
- Benim için yaz
- Bize ürününüzden ve onu benzersiz kılanın ne olduğundan bahsedin!
- Yapay zekanın sizin için etkileyici başlıklar oluşturmasına izin verin
- Ürün adı
Tüm ürün incelemelerinizi görmek için okunmamış filtresini devre dışı bırakmayı deneyin
Okunmamış ürün incelemesi yok
- Ton seçildi
İkna edici
Çiçekli
Resmi
Gelişigüzel
- Ürününüzün sunumunu markanızla uyumlu biçimde şekillendirmek için tonu ve sesi belirleyin.
Ton ve ses
Ayrıntılar
- Ürün açıklaması
Ürün adı
- Aşağıdaki ayrıntıları daha sonra her zaman değiştirebilirsiniz.
Önizleme
- Ürünler Ayrıntıları Oluştur
- Ton ve sesi ayarla
- Ürününüzün internette bulunmasına yardımcı olmak için temel özellikler, avantajlar veya ayrıntılar ekleyin.
Örneğin, yumuşak kumaş, dayanıklı dikiş, benzersiz tasarım
- Ürününüzü benzersiz kılan özellikleri vurgulayın ve yapay zekanın işini yapmasına izin verin.
- Ürününüz hakkında
- Bir ad öner
- Örnek: Yumuşak kumaş, dayanıklı dikiş, benzersiz tasarım.
- Ürün adı
- Ya da daha fazla isim önerisi için dokunarak seçeneklerinizi genişletin.
- Ürün adınızı ekleyin
Yapay zeka destekli. <a href=\'guidelines\'><u>Daha fazla bilgi edinin</u></a>.
Bir ürünü ve ayrıntılarını manuel olarak ekleyin
El ile ekle
@@ -1054,7 +1079,6 @@ Language: tr
Özel tekliflerle satışları artırın
Mağazanızı görüntüleme
Güncel kalın
- Mobil ödemelere katılın
Yöneticide daha fazlasını yönetin
Genel
Ayarlar
diff --git a/WooCommerce/src/main/res/values-zh-rCN/strings.xml b/WooCommerce/src/main/res/values-zh-rCN/strings.xml
index a2a4dd9aa38..6775b095578 100644
--- a/WooCommerce/src/main/res/values-zh-rCN/strings.xml
+++ b/WooCommerce/src/main/res/values-zh-rCN/strings.xml
@@ -1,11 +1,71 @@
+ 密钥无效:请删除开头的“_”字符。
+ 此密钥已被用于另一个自定义字段。\n该应用程序暂不支持创建重复密钥。 如果需要,请使用 wp-admin 复制密钥。
+ 添加自定义字段
+ 自定义字段已删除
+ 更改保存失败,请重试
+ 更改已保存
+ 正在保存更改
+ 您似乎未连接到互联网。 请确保您的 Wi-Fi 已打开。 如果您要使用移动数据,请确保已在设备设置中将其启用。
+ 扫描失败。 请稍后再试
+ 字段值
+ 密钥
+ 其他产品类型(如可变产品和虚拟产品)将在今后的更新中得到支持。
+ POS 目前仅可用于简单实体产品。
+ 取消
+ 时长
+ 活动将持续进行,直到您手动将其停止。
+ 指定时长
+ 至 %1$s
+ 安排
+ 每日支出
+ 您希望在广告活动上投入多少资金?活动应持续多长时间?
+ %1$s ➔ %2$s
+ 通过 Blaze 让数百万人看到您的产品,提高您的销售额
+ 想要提高您的销售额?
+ 加载自定义字段时出错
+ 自定义字段
+ 背景变暗。 轻点即可关闭对话框。
+ 每周 %1$s
+ 在手动停止前保持运行
+ 从 %1$s 起持续进行
+ 每周支出
+ 从 %2$s 起每周 %1$s
+ 每周
+ 剩余
+ 总计
+ 点击率
+ 您的设备似乎处于省电模式。 \n当您的商店处于活动状态时,我们无法提供其相关信息
+ 带选项的弹出菜单。 轻扫即可浏览商品。
+ 打开工具栏菜单
+ 显示读卡器状态的工具栏。 菜单已打开。 轻点两下即可进行交互。
+ 显示读卡器状态的工具栏。 轻点两下即可进行交互。
+ 菜单已禁用
+ 菜单已启用
+ 读卡器未连接。 轻点两下即可连接
+ 读卡器已连接
+ 背景变暗。 轻点即可关闭菜单。
+ 付款成功复选标记图标
+ 将此商品从购物车中移除
+ 任何进行中的订单都将丢失。
+ 退出销售点模式?
+ 关闭
+ 背景变暗。 轻点即可关闭对话框。
+ 轻点两下即可关闭对话框
+ 仅限简单产品的对话框
+ 轻点两下即可了解更多信息
+ 仅限简单产品的横幅
+ 立即使用 Blaze Ads 推广您的产品并提高销售额。
+ 提高您的销售额
+ 随时随地接受付款
+ 输入的 PIN 错误。 再次尝试,或使用另一种付款方式
菜单
购物车中的产品 %s,价格 %s
产品 %s,价格 %s
@@ -14,8 +74,7 @@ Language: zh_CN
新订单
确定
+ 在商店管理中创建订单
- 要接受非简单产品的付款,请退出 POS,然后在订单选项卡中创建新订单。
- POS 目前仅可用于简单实体产品。\n其他产品类型(如可变产品和虚拟产品)将在今后的更新中得到支持。
+ 要接受非简单产品的付款,请退出 POS,然后在订单选项卡中创建新订单。
为何看不到我的产品?
信息
关闭
@@ -39,8 +98,6 @@ Language: zh_CN
POS 目前仅支持简单产品 – \n创建一个以开始。
未找到受支持的产品
无任何产品
- 让我们为客户服务
- 开始
获取支持
连接您的读卡器
照片已删除
@@ -135,8 +192,7 @@ Language: zh_CN
无数量规则
受众
取消
- 退出
- 是否确定要退出 POS 机?
+ 退出
退出 POS 机
将 %s 从购物车中移除
结账
@@ -419,20 +475,16 @@ Language: zh_CN
标语
更换图片
应用
- 开始
+ 开始日期
%1$s 天
- 设置时长
展示次数反映了您的广告出现在潜在客户面前的频率。\n\n\n 由于在线流量的波动和用户行为的差异,我们无法确保确切的数字,但我们将致力于使您广告的实际展示次数尽可能接近目标次数。\n\n\n 请记住,展示次数关乎可见性,而非查看者的操作。
完成
展示次数
更新
编辑
- 时长
每天的预计受众人数
%1$s 每天
共计 %1$s 天
- 总支出
- 您愿意在产品推广活动上花费多少资金?
设置您的预算
所有
从 %2$s 起 %1$s 天
@@ -634,7 +686,6 @@ Language: zh_CN
通过 Blaze 提高您商店的销售额
刷新广告活动列表时出错。 请稍后重试。
选择媒体来源
- 生成产品名称和描述时出错。
未检测到文本。 请选择其他包装照片或手动输入产品详细信息。
添加产品
扫描条形码
@@ -656,9 +707,6 @@ Language: zh_CN
尝试通过借记卡或信用卡完成一次 %s 付款。\n完成后将退款。
整个过程简单、安全且私密。
您可以直接在手机上\n接受所有类型的现场付款。 无需额外的硬件。
- 预算
- 点击次数
- 展示次数
已拒绝
已完成
已启用
@@ -691,46 +739,23 @@ Language: zh_CN
设置
使用电子邮件手动添加详情
我们无法处理您的应用程序登录请求
- 复制产品名称到剪贴板时出错。
- 产品名称已复制到剪贴板。
- AI 生成的产品名称
复制产品描述到剪贴板时出错。
产品描述已复制到剪贴板。
产品生成失败。 请重试
清除地址并停止使用该费率
为该订单设置新税率
自动添加税率
- 生成产品名称时出现问题。 请重试。
- 重新生成
- 为我写作
- 介绍您的产品及其独特之处!
- 让 AI 为您生成有吸引力的标题
- 产品名称
尝试禁用未读过滤器,以查看所有产品评论
无未读产品评论
- 所选语调
自信
华丽
正式
随意
- 设置语调和声音,确保产品展现形式符合品牌调性。
语调和声音
详情
- 产品描述
产品名称
- 您可以随时更改下方的详细信息。
预览
- 创建产品详情
- 设置语调和声音
- 添加关键功能、优势或详情,提高产品在网上的曝光率。
例如,柔软的面料、结实的缝线、独特的设计
- 突出显示您产品的独特之处,让 AI 创造奇迹。
- 关于您的产品
- 提供名称建议
- 例如:柔软的面料、结实的缝线、独特的设计。
- 产品名称
- 或者可以轻点以获得更多名称建议,扩大您的选择范围。
- 添加产品名称
由 AI 提供支持。 <a href=\'guidelines\'><u>了解更多</u></a>。
手动添加产品和详情
手动添加
@@ -1054,7 +1079,6 @@ Language: zh_CN
使用特别优惠增加销售额
查看商店
保持更新
- 加入移动付款功能
在“管理员”上管理更多内容
常规
设置
diff --git a/WooCommerce/src/main/res/values-zh-rTW/strings.xml b/WooCommerce/src/main/res/values-zh-rTW/strings.xml
index e88dc78a949..eec91024a13 100644
--- a/WooCommerce/src/main/res/values-zh-rTW/strings.xml
+++ b/WooCommerce/src/main/res/values-zh-rTW/strings.xml
@@ -1,11 +1,71 @@
+ 無效的金鑰:請移除開頭的「_」字元。
+ 此金鑰已用於其他自訂欄位。\n應用程式目前不支援建立重複的金鑰。 必要時請使用 wp-admin 複製金鑰。
+ 新增自訂欄位
+ 自訂欄位已刪除
+ 無法儲存變更,請再試一次
+ 變更已儲存
+ 正在儲存變更
+ 你目前沒有網際網路連線。 請確認 Wi-Fi 已開啟。 如果你使用行動數據,請確認已在裝置設定中啟用。
+ 掃描失敗。 請稍後再試一次
+ 值
+ 金鑰
+ 待未來更新後,將可提供多款式與虛擬商品等其他商品種類的資訊。
+ 目前只有簡單實體商品能使用 POS。
+ 取消
+ 時間長度
+ 行銷活動會持續執行,直到你停止為止。
+ 指定期間
+ 至 %1$s
+ 排程
+ 每日花費
+ 你想要在行銷活動上花費多少費用,希望活動持續多長時間?
+ %1$s ➔ %2$s
+ 運用 Blaze 讓數百萬使用者看見你的商品,並強化銷售表現
+ 想要強化銷售表現嗎?
+ 載入自訂欄位時發生錯誤
+ 自訂欄位
+ 背景已調暗。 點選以關閉對話方塊。
+ 每週 %1$s
+ 持續執行,直到我停止為止
+ 從 %1$s起持續進行
+ 每週花費
+ 從 %2$s起,每週 %1$s
+ 每週
+ 剩餘
+ 總計
+ 點擊率
+ 看來你的裝置現已開啟省電模式。 \n我們無法在該模式啟用時提供商店資訊
+ 附選項的快顯選單。 滑動以導覽項目。
+ 開啟工具列選單
+ 顯示讀卡機狀態的工具列。 選單已開啟。 點選兩下以互動。
+ 顯示讀卡機狀態的工具列。 點選兩下以互動。
+ 選單已停用
+ 選單已啟用
+ 讀卡機未連結。 點選兩下以連結
+ 讀卡機已連結
+ 背景已調暗。 點選以關閉選單。
+ 付款成功核取記號圖示
+ 從購物車將此商品移除
+ 任何進行中的訂單將會遺失。
+ 要結束銷售時點情報系統模式嗎?
+ 關閉
+ 背景已調暗。 點選以關閉對話方塊。
+ 點選兩下以關閉對話框
+ 僅限簡單商品的對話框
+ 點選兩下以深入了解
+ 僅限簡單商品的橫幅
+ 運用 Blaze 廣告宣傳商品,立即提升銷售表現。
+ 強化銷售表現
+ 隨時隨地輕鬆收款
+ 輸入的 PIN 碼不正確。 請再試一次,或使用其他付款方式
選單
購物車「%s」中的商品,價格為 %s
商品「%s」的價格為 %s
@@ -14,8 +74,7 @@ Language: zh_TW
新訂單
確定
+ 在商店管理建立訂單
- 若要針對簡單商品以外的品項收款,請將 POS 結束,然後從訂單分頁建立新訂單。
- 目前只有簡單實體商品能使用 POS。\n待未來更新後,將可提供多款式與虛擬商品等其他商品種類的資訊。
+ 若要針對簡單商品以外的品項收款,請將 POS 結束,然後從訂單分頁建立新訂單。
為什麼我無法查看自己的商品?
資訊
關閉
@@ -39,8 +98,6 @@ Language: zh_TW
POS 目前僅支援簡單商品 – \n建立商品後便可開始建立行銷活動。
找不到支援的商品
沒有商品
- 開始為客戶提供服務
- 開始
取得支援
連線至你的讀卡機
已移除相片
@@ -135,8 +192,7 @@ Language: zh_TW
沒有數量規則
受眾
取消
- 結束
- 你確定要結束 POS 嗎?
+ 結束
結束 POS
從購物車移除「%s」
結帳
@@ -419,20 +475,16 @@ Language: zh_TW
標語
變更圖片
套用
- 開始時間
+ 開始日期
%1$s 天
- 設定期間
曝光數反映了廣告向潛在顧客顯示的頻率。\n\n\n 因為線上流量和使用者行為會波動,無法確定確切數字,但我們會致力讓廣告的實際曝光數盡可能符合你的目標次數。\n\n\n 切記,曝光數與能見度有關,而非觀眾採取的行動。
完成
曝光數
更新
編輯
- 時間長度
預估每天觸及的人數
每日 %1$s
%1$s 天
- 總預算
- 你打算在商品推廣行銷活動上花費多少預算?
設定預算
全部
從 %2$s起共 %1$s 天
@@ -634,7 +686,6 @@ Language: zh_TW
使用 Blaze 為你的商店帶動更多銷售業績
There was an error refreshing the list of campaigns. 請稍後再試。
選取媒體來源
- An error occurred while generating product name & description.
未偵測到文字。 請選取另一張包裹照片或手動輸入產品詳細資訊。
新增商品
掃瞄條碼
@@ -656,9 +707,6 @@ Language: zh_TW
嘗試使用簽帳金融卡或信用卡支付 %s 款項。\n完成後就會退款。
簡單、安全且私密。
接受所有類型的親自收款,\n全都能在手機上完成。 無須使用額外硬體。
- 預算
- 點擊數
- 曝光數
已拒絕
已完成
已啟用
@@ -691,46 +739,23 @@ Language: zh_TW
設定
使用電子郵件手動新增詳情
我們無法處理你的應用程式登入要求
- 複製商品名稱到剪貼簿時發生錯誤。
- 商品名稱已複製到剪貼簿。
- AI 產生的商品名稱
複製商品說明到剪貼簿時發生錯誤。
商品說明已複製到剪貼簿。
商品產生失敗。 請再試一次
清除地址並停止使用此稅率
為此訂單設定新稅率
自動新增稅率
- 產生產品名稱時發生問題, 請再試一次。
- 重新產生
- 幫我撰寫
- 介紹你的產品與其獨特之處!
- 讓 AI 為你產生吸引人的名稱
- 產品名稱
嘗試停用「未讀」篩選條件,便可看到所有產品評論
沒有未讀的產品評論
- 已選取語氣
具有說服力
誇飾華麗
正式
休閒
- 設定語氣和風格,讓產品反映品牌形象。
語氣和風格
詳細資料
- 產品說明
產品名稱
- 稍後也可隨時變更下方詳細資料。
預覽
- 建立產品詳細資料
- 設定語氣和風格
- 新增主要功能、優點或詳細資料,可以讓人更容易在網路上找到你的產品。
例如布料柔軟、強固縫線、獨特設計
- 列出產品獨特之處,AI 便能產生絕佳內容。
- 產品相關資訊
- 建議名稱
- 例如布料柔軟、強固縫線、獨特設計
- 產品名稱
- 或點選更多名稱建議,查看更多選項。
- 新增產品名稱
AI 技術支援。 <a href=\'guidelines\'><u>深入瞭解</u></a>。
手動新增產品和詳細資料
手動新增
@@ -1054,7 +1079,6 @@ Language: zh_TW
透過特價提高銷售額
檢視你的商店
掌握最新資訊
- 加入行動付款
在管理員上管理更多功能
一般
設定
diff --git a/WooCommerce/src/main/res/values/colors_base.xml b/WooCommerce/src/main/res/values/colors_base.xml
index 26cf9727e7b..8589722e4fe 100644
--- a/WooCommerce/src/main/res/values/colors_base.xml
+++ b/WooCommerce/src/main/res/values/colors_base.xml
@@ -195,7 +195,6 @@
- @color/color_secondary
@color/jetpack_green_40
@color/woo_purple_50
diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml
index 50eebb0073c..db805f6d5fa 100644
--- a/WooCommerce/src/main/res/values/strings.xml
+++ b/WooCommerce/src/main/res/values/strings.xml
@@ -1015,6 +1015,7 @@
Deleted shipment tracking
Error deleting tracking
Tracking deleted
+ Scanning failed. Please try again later
If renaming an add-on in your web dashboard, please note that previous orders will no longer show that add-on within the app.
View add-ons from your device!
We are working on making it easier for you to see product add-ons from your device! For now, you’ll be able to see the add-ons for your orders. You can create and edit these add-ons in your web dashboard.
@@ -2346,6 +2347,7 @@
Choose from device
Take a photo
WordPress media library
+ Choose an existing product photo
Device Media Library
Select Media Source
WordPress media library
@@ -3485,6 +3487,7 @@
-->
Please log in to the WooCommerce app
Your network is unavailable.\nCheck your data or wifi connection.
+ Looks like your device is in Battery Saver mode. \nWe can\'t provide your store information while it\'s active
Store analytics not available! Please upgrade to the latest version of WooCommerce to view your store analytics.
Today\'s store stats
WooCommerce Stats Today
@@ -3786,6 +3789,8 @@
-->
Boost your sales
Promote your products with Blaze Ads and increase your sales now.
+ Thinking about boosting your sales?
+ Get your products seen by millions with Blaze and boost your sales
Set your budget
- How much would you like to spend on your product promotion campaign?
- Total spend
+ How much would you like to spend on your campaign, and how long should it run for?
+ weekly spend
+ Daily spend
for %1$s days
%1$s daily
Estimated people reached per day
- Duration
+ Schedule
+ Ongoing from %1$s
Edit
Update
Impressions
@@ -3923,11 +3933,17 @@
Impressions reflect the frequency with which your ad appears to potential customers.\n\n
While exact numbers can\'t be assured due to fluctuating online traffic and user behavior, we aim to match your ad\'s actual impressions as closely as possible to your target count.\n\n
Remember, impressions are about visibility, not action taken by viewers.
- Set duration
%1$s days
- Starts
+ to %1$s
+ Start date
+ Run until I stop it
+ Specify the duration
+ Campaign will run until you stop it.
+ Duration
Apply
+ Cancel
Failed to estimate impressions. Retry?
+ %1$s weekly
+ Custom Fields
+ Error while loading custom fields
+ Saving changes
+ Changes saved
+ Saving changes failed, please try again
+ Custom Field deleted
+ View and edit Custom Fields
+ When saving changes to custom fields, they will take effect immediately.
+ Add custom fields
+ Key
+ Value
+ This key is already used for another custom field.\nThe app currently does not support creating duplicate keys. Please use wp-admin to duplicate a key if needed.
+ Invalid key: please remove the \"_\" character from the beginning.
+ Copy Key
+ Copy Value
diff --git a/WooCommerce/src/main/res/values/styles_base.xml b/WooCommerce/src/main/res/values/styles_base.xml
index f5610796f05..147aa52a894 100644
--- a/WooCommerce/src/main/res/values/styles_base.xml
+++ b/WooCommerce/src/main/res/values/styles_base.xml
@@ -533,16 +533,6 @@ theme across the entire app. Overridden versions should be added to the styles.x
- ?colorPrimary
-
-
-
-
-