diff --git a/.agents/skills/maestro-flow-doctor/SKILL.md b/.agents/skills/maestro-flow-doctor/SKILL.md new file mode 100644 index 000000000000..d236cf092fd9 --- /dev/null +++ b/.agents/skills/maestro-flow-doctor/SKILL.md @@ -0,0 +1,41 @@ +--- +name: maestro-flow-doctor +description: Repair a failing WooCommerce Android Maestro flow by reproducing it against the lab store, inspecting Maestro selectors, patching the smallest selector/wait/setup issue, and rerunning with repeat evidence. Human-triggered only. +allowed-tools: Bash, Read, Edit, Grep, Glob +user-invocable: true +--- + +# Maestro Flow Doctor + +Use this skill only for repairing existing Maestro smoke flows under `.maestro/flows/`. + +## Ground Rules + +- Always run against the lab store: pass `--store lab`. +- Never run destructive repair loops against the shared store. +- Never ask the user to paste credentials. Validate that `.maestro/.env.local` has the required variable names without echoing values. +- Use Maestro MCP as the selector source of truth: `run` executes the YAML we ship, and `inspect_screen` shows the hierarchy Maestro selectors see. +- Keep fixes minimal: selector, wait, setup, or fixture query changes only. Do not broaden coverage while repairing a flake. +- Promotion still requires burst evidence; a local `--repeat` pass is repair evidence, not promotion. + +## Workflow + +1. Confirm the target flow path exists under `.maestro/flows/`. +2. Run syntax and coverage checks: + - `.maestro/scripts/check-smoke-coverage.py` + - `maestro test --dry-run ` if supported by the installed Maestro version; otherwise continue with a real lab run. +3. Reproduce on a lab-store device: + - `.maestro/scripts/run-smoke-tests.sh --store lab --include-tags flaky_quarantine ` +4. At the failure point, use Maestro MCP `inspect_screen`. +5. Compare the failing selector with the hierarchy: + - Prefer `id:` selectors exposed through `testTag`. + - Use generated `strings.env` values for text assertions. + - Do not add `point:` selectors unless the flow comment explains why no semantic selector exists. +6. Patch the smallest file set. +7. Rerun the single flow with repeat evidence: + - `.maestro/scripts/run-smoke-tests.sh --store lab --repeat 3 ` +8. Summarize: + - root cause, + - files changed, + - repeat result, + - whether the flow remains quarantined. diff --git a/.agents/skills/smoke-tests/SKILL.md b/.agents/skills/smoke-tests/SKILL.md new file mode 100644 index 000000000000..ebeaa3e2c91f --- /dev/null +++ b/.agents/skills/smoke-tests/SKILL.md @@ -0,0 +1,113 @@ +--- +name: smoke-tests +description: Prepare the local environment for the Maestro smoke-test suite — verify tooling, select a device, collect the APK if needed, validate the .env file by variable name only, then hand the user the exact lab-store CLI command to run or run it on request. +allowed-tools: Bash, Read, Edit, Write, Grep, Glob, AskUserQuestion +user-invocable: true +--- + +# Prepare & launch the Maestro smoke-test suite + +This skill is a **setup + handoff** flow. Its job is to get everything ready so `.maestro/scripts/run-smoke-tests.sh` will work on the first try, then give the user the CLI command. + +It does NOT own the test-runner mechanics. Ordering, per-flow recording, report generation, and the "recordings are kept only for failures, outside the repo" contract all live in the script itself. + +## Scope (what this skill is responsible for) + +1. **Tooling** — confirm `maestro` and `adb` are on PATH. If not, tell the user how to install them and stop. +2. **Device** — confirm at least one Android device/emulator is attached (`adb devices`). If several are attached, choose the runner `--device` value with the user. +3. **APK** — ask whether to use the currently installed app or install an APK; install only if requested. +4. **`.env.local`** — confirm `.maestro/.env.local` exists and contains every variable the selected command references. List missing variable names only. +5. **Handoff** — print the exact CLI command. If the user explicitly asks ("run it", "go ahead", etc.), invoke the script for them and stream its output. + +Everything below the handoff — P2 ordering, store selection, seed/cleanup, animation restore, lock handling, retry accounting, artifact policy, HTML + JUnit reports — is handled by `.maestro/scripts/run-smoke-tests.sh`. + +## Steps + +### 1. Check tooling + +Run `command -v maestro` and `command -v adb`. If either is missing: + +- maestro → `curl -fsSL "https://get.maestro.mobile.dev" | bash` (requires Java 17+). +- adb → install Android SDK platform-tools and add to PATH. + +Tell the user which is missing and how to install it, then stop. + +### 2. Ensure an emulator is running + +Run `adb devices`. Count the lines whose second column is `device`. + +- **0 devices:** list AVDs with `emulator -list-avds`. + - If none → tell the user to create one in Android Studio (AVD Manager) and stop. + - If exactly one → ask the user if they want to boot it, then `emulator -avd -no-snapshot-save &` (backgrounded), `adb wait-for-device`, and poll `adb shell getprop sys.boot_completed` until `1` (up to ~90s). + - If multiple → ask which one to boot. +- **1+ devices:** proceed. + +### 3. Ask whether to install an APK + +Default to the currently installed app. Ask for an APK only when the user wants to test a specific build. + +Prompt the user for one of: + +- An absolute or repo-relative path to an `.apk` on disk. +- A drag-and-drop attachment (Claude Code exposes it as a temp path like `/tmp/.../file.apk`). +- The word `build` — in which case run `./gradlew :WooCommerce:assembleWasabiDebug` in the foreground and use `WooCommerce/build/outputs/apk/wasabi/debug/WooCommerce-wasabi-debug.apk`. + +Validate the chosen APK: + +- File exists on disk. +- Extension is `.apk`. +- `aapt dump badging | head -1` reports `package: name='com.woocommerce.android.dev'` (the wasabi build). If it reports a different package, stop and ask the user whether to continue — a non-wasabi APK will fail the `appId` check on the first flow. + +Install it: `adb install -r -g `. If the install fails with `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, tell the user a previous build with a different signature is already installed and ask whether to uninstall first (`adb uninstall com.woocommerce.android.dev`). Do not uninstall without confirmation. + +### 4. Validate `.maestro/.env.local` + +For default lab-store runs, the core required vars are: + +- `MAESTRO_WOO_LAB_STORE_URL` +- `MAESTRO_WOO_LAB_EMAIL` +- `MAESTRO_WOO_LAB_PASSWORD` +- `MAESTRO_WOO_LAB_CONSUMER_KEY` +- `MAESTRO_WOO_LAB_CONSUMER_SECRET` + +Shared-store runs require the matching `MAESTRO_WOO_SHARED_*` variables and must be explicit with `--store shared`. Additional `MAESTRO_WOO_*` vars are required per-flow (for example `MAESTRO_WOO_NOT_A_WOO_STORE_URL` for `login_not_woo_store.yaml`, `MAESTRO_WOO_JN_*` for `login_no_jetpack.yaml`). The canonical list lives in `.maestro/env.example`. + +To build the list of actually-referenced vars, grep flow YAMLs for `\${WOO_[A-Z_]+}` under `.maestro/flows/` and check each one resolves to a non-empty value in `.maestro/.env.local`. + +If the file is missing or any required var is empty: + +1. List the missing variable names and their purpose (mirror `.maestro/env.example`). +2. Ask the user to edit `.maestro/.env.local` directly. +3. Do NOT proceed until every referenced var resolves. Re-read the file after the user updates it. + +Never commit `.env.local`. Never ask the user to paste secret values into chat. Never echo secret values back to the user. + +### 5. Hand off the CLI command + +Once steps 1–4 all pass, print the command the user should run: + +``` +.maestro/scripts/run-smoke-tests.sh --store lab +``` + +Tell the user: + +- The default command runs `smoke_core` only and excludes `flaky_quarantine`. +- Use `--include-tags smoke_extended` or a single flow path for provisional repair work. +- Use `--store shared` only for non-destructive ad-hoc runs; destructive shared-store runs are CI-only under the store lock. +- The runner seeds fixtures, writes a manifest, and cleans exactly those IDs on exit. +- It captures and restores device animation settings. +- Screen recordings are kept only for failed lab-store flows; shared-store credential paths use screenshots only. +- Artifacts (recordings, logs, HTML + JUnit report) are written OUTSIDE the repo, under `$HOME/woocommerce-maestro-output//` by default. Override with `--output-dir ` or the `WOO_MAESTRO_OUTPUT_DIR` env var. +- The HTML report auto-opens at the end on macOS, or can be opened manually from the path the script prints. + +If the user asks to run it ("go ahead", "run it", "yes please", etc.), invoke the script yourself via `Bash` and stream its output. Pass `--apk` only if you installed an APK in step 3. + +When the script exits, read the last line of its output (it prints `Report:` and `Result:` summary lines) and relay a one-line summary to the user plus the clickable `file://` path to the HTML report. If any flows failed, call out the first failing flow by name — it's usually the most actionable one. + +## Notes + +- The skill's job ends at handoff. Don't reinvent the test-runner behaviour here — if something about per-flow recording, ordering, or artifact location needs to change, change it in `.maestro/scripts/run-smoke-tests.sh`. +- `.env.local` is git-ignored. Never stage or commit it, even if the user asks you to save their credentials. +- Artifacts default to `$HOME/woocommerce-maestro-output/` (outside the repo) — the repo's `.gitignore` still excludes the legacy `.maestro/output/` path for safety. +- Do NOT parallelize. `adb shell screenrecord` only supports one invocation per device, and Maestro runs one flow at a time against a single emulator. The script runs sequentially by design. diff --git a/.buildkite/commands/run-maestro-tests.sh b/.buildkite/commands/run-maestro-tests.sh new file mode 100755 index 000000000000..d80ee87cabc4 --- /dev/null +++ b/.buildkite/commands/run-maestro-tests.sh @@ -0,0 +1,93 @@ +#!/bin/bash +set -euo pipefail + +# Run the Maestro smoke suite through the repository runner. +# +# Expected CI secrets: +# MAESTRO_WOO_SHARED_JETPACK_STORE_URL +# MAESTRO_WOO_SHARED_WPCOM_EMAIL +# MAESTRO_WOO_SHARED_WPCOM_PASSWORD +# MAESTRO_WOO_SHARED_CONSUMER_KEY (when MAESTRO_SEED=true) +# MAESTRO_WOO_SHARED_CONSUMER_SECRET (when MAESTRO_SEED=true) +# +# Optional controls: +# MAESTRO_STORE=shared|lab +# MAESTRO_INCLUDE_TAGS=smoke_core,smoke_extended,destructive +# MAESTRO_EXCLUDE_TAGS=flaky_quarantine,pos_tablet +# MAESTRO_REPEAT=3 +# MAESTRO_APK_PATH=/path/to/beta.apk +# MAESTRO_SEED=true + +if [[ "${BUILDKITE_PULL_REQUEST:-false}" != "false" ]] && + .buildkite/commands/should-skip-job.sh --job-type validation; then + echo "Skipping Maestro tests - no relevant changes" + exit 0 +fi + +source .maestro/scripts/configure-toolchain.sh + +STORE="${MAESTRO_STORE:-shared}" +INCLUDE_TAGS="${MAESTRO_INCLUDE_TAGS:-smoke_core}" +REPEAT="${MAESTRO_REPEAT:-1}" +OUTPUT_DIR="${MAESTRO_OUTPUT_DIR:-WooCommerce/build/outputs/maestro-smoke}" +SEED_ARGS=() +EXCLUDE_TAGS_ARGS=() +if [[ "${MAESTRO_SEED:-false}" == "true" ]]; then + SEED_ARGS+=(--seed) +fi +if [[ -n "${MAESTRO_EXCLUDE_TAGS:-}" ]]; then + EXCLUDE_TAGS_ARGS+=(--exclude-tags "$MAESTRO_EXCLUDE_TAGS") +fi + +if [[ -n "${MAESTRO_APK_PATH:-}" ]]; then + APK_PATH="$MAESTRO_APK_PATH" +else + echo "--- Building and installing wasabi debug APK" + "$(dirname "${BASH_SOURCE[0]}")/restore-cache.sh" + ./gradlew :WooCommerce:installWasabiDebug + APK_PATH="" +fi + +echo "--- Running Maestro smoke tests" +set +e +if [[ -n "$APK_PATH" ]]; then + .maestro/scripts/run-smoke-tests.sh \ + --store "$STORE" \ + --include-tags "$INCLUDE_TAGS" \ + --repeat "$REPEAT" \ + --output-dir "$OUTPUT_DIR" \ + "${SEED_ARGS[@]}" \ + "${EXCLUDE_TAGS_ARGS[@]}" \ + --no-open \ + --apk "$APK_PATH" +else + .maestro/scripts/run-smoke-tests.sh \ + --store "$STORE" \ + --include-tags "$INCLUDE_TAGS" \ + --repeat "$REPEAT" \ + --output-dir "$OUTPUT_DIR" \ + "${SEED_ARGS[@]}" \ + "${EXCLUDE_TAGS_ARGS[@]}" \ + --no-open +fi +MAESTRO_EXIT_STATUS=$? +set -e + +echo "--- Collecting Maestro results" +mkdir -p WooCommerce/build/buildkite-test-analytics +LATEST_REPORT_DIR="$(find "$OUTPUT_DIR" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1)" +if [[ -n "$LATEST_REPORT_DIR" && -f "$LATEST_REPORT_DIR/report.xml" ]]; then + cp "$LATEST_REPORT_DIR/report.xml" WooCommerce/build/buildkite-test-analytics/maestro-report.xml +fi +if [[ "${BUILDKITE:-false}" == "true" && -n "$LATEST_REPORT_DIR" && -f "$LATEST_REPORT_DIR/report.xml" ]]; then + python3 .maestro/scripts/annotate-run.py --junit "$LATEST_REPORT_DIR/report.xml" | + buildkite-agent annotate --context maestro-smoke --style info || + echo "Warning: could not publish the Maestro Buildkite annotation." >&2 +fi + +if [[ "$MAESTRO_EXIT_STATUS" -ne 0 ]]; then + echo "^^^ +++" + echo "Maestro smoke tests were not clean. Check flaky/failure details in artifacts." +fi + +exit "$MAESTRO_EXIT_STATUS" diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 5b0d301866d9..6c7291310479 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -117,6 +117,24 @@ steps: artifact_paths: - "**/build/instrumented-tests/**/*" + - input: Run Maestro smoke tests? + prompt: "Run on-demand Maestro smoke tests. Requires MAESTRO_WOO_* secrets and an Android device/emulator." + key: maestro_smoke_tests_triggered + + - label: ":maestro: Maestro smoke tests" + depends_on: maestro_smoke_tests_triggered + command: .buildkite/commands/run-maestro-tests.sh + concurrency: 1 + concurrency_group: "woocommerce-android/maestro/shared-store" + plugins: + - $CI_TOOLKIT + - $TEST_COLLECTOR : + <<: *test_collector_common_params + api-token-env-name: "BUILDKITE_ANALYTICS_TOKEN_MAESTRO_TESTS" + artifact_paths: + - "WooCommerce/build/outputs/maestro-smoke/**/*" + - "WooCommerce/build/buildkite-test-analytics/maestro-report.xml" + - label: "🐘 Populate Gradle build cache" command: .buildkite/commands/gradle-cache-build.sh diff --git a/.buildkite/release-pipelines/maestro-smoke.yml b/.buildkite/release-pipelines/maestro-smoke.yml new file mode 100644 index 000000000000..896b01c9d7b1 --- /dev/null +++ b/.buildkite/release-pipelines/maestro-smoke.yml @@ -0,0 +1,27 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +--- + +agents: + queue: "android" + +env: + MAESTRO_STORE: "shared" + MAESTRO_INCLUDE_TAGS: "smoke_core,smoke_extended,destructive" + MAESTRO_EXCLUDE_TAGS: "flaky_quarantine,pos_tablet" + MAESTRO_REPEAT: "1" + MAESTRO_SEED: "true" + +steps: + - label: ":maestro: Release Maestro smoke tests" + command: .buildkite/commands/run-maestro-tests.sh + concurrency: 1 + concurrency_group: "woocommerce-android/maestro/shared-store" + plugins: + - $CI_TOOLKIT + - $TEST_COLLECTOR : + files: "WooCommerce/build/buildkite-test-analytics/*.xml" + format: "junit" + api-token-env-name: "BUILDKITE_ANALYTICS_TOKEN_MAESTRO_TESTS" + artifact_paths: + - "WooCommerce/build/outputs/maestro-smoke/**/*" + - "WooCommerce/build/buildkite-test-analytics/maestro-report.xml" diff --git a/.buildkite/schedules/maestro-smoke-burst.yml b/.buildkite/schedules/maestro-smoke-burst.yml new file mode 100644 index 000000000000..7c5eb99bbcb6 --- /dev/null +++ b/.buildkite/schedules/maestro-smoke-burst.yml @@ -0,0 +1,39 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +--- + +# Intended Buildkite schedule: +# Bi-weekly Thursday night before the Monday production rollout. +# Branch: active release branch. +# +# This is deliberately not a nightly pipeline. + +agents: + queue: "android" + +env: + MAESTRO_STORE: "shared" + MAESTRO_INCLUDE_TAGS: "smoke_core,smoke_extended,destructive" + MAESTRO_EXCLUDE_TAGS: "flaky_quarantine,pos_tablet" + MAESTRO_REPEAT: "3" + MAESTRO_SEED: "true" + +steps: + - label: ":maestro: Thursday Maestro smoke burst" + command: .buildkite/commands/run-maestro-tests.sh + concurrency: 1 + concurrency_group: "woocommerce-android/maestro/shared-store" + plugins: + - $CI_TOOLKIT + - $TEST_COLLECTOR : + files: "WooCommerce/build/buildkite-test-analytics/*.xml" + format: "junit" + api-token-env-name: "BUILDKITE_ANALYTICS_TOKEN_MAESTRO_TESTS" + artifact_paths: + - "WooCommerce/build/outputs/maestro-smoke/**/*" + - "WooCommerce/build/buildkite-test-analytics/maestro-report.xml" + +notify: + - slack: + channels: + - "#woo-android-notifs" + if: "build.state != 'passed'" diff --git a/.gitignore b/.gitignore index 792b6202117c..e8b8b6dbfcc7 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ local.properties # Backup Files *.bak +__pycache__/ # Android Studio Navigation editor temp files .navigation/ @@ -129,3 +130,12 @@ google-upload-credentials.json .grepai/ docs/superpowers + +# Maestro smoke-test local files and artifacts. +.env.local +.maestro/output/ +.maestro/tmp/ +.maestro/report.xml +.maestro/*.maestro-screenshots/ +*.maestro-screenshots/ +report.xml diff --git a/.maestro/README.md b/.maestro/README.md new file mode 100644 index 000000000000..a470a0d0a329 --- /dev/null +++ b/.maestro/README.md @@ -0,0 +1,155 @@ +# Maestro Smoke Tests + +Automated UI smoke tests for the WooCommerce Android P2 checklist: +https://woomobilep2.wordpress.com/flows-for-app-features-smoke-testing/ + +## Operating Model + +The suite has two store targets: + +- `lab`: default for local development, repair loops, can-fail checks, and destructive iteration. Use an + automation-owned WooCommerce store that is connected to Jetpack/WP.com with a dedicated WP.com test account. +- `shared`: `inpersonpayments.wpcomstaging.com`, used for release-tool runs, Thursday burst runs, and explicit + non-destructive developer runs. + +The no-Jetpack login scenario uses its own `MAESTRO_WOO_NO_JETPACK_*` variables. Do not reuse those Jurassic Ninja +site credentials as the `lab` store block when running the broader suite. + +Destructive flows against the shared store are refused outside CI. In CI, they require `--seed`, the complete +`MAESTRO_WOO_SHARED_*` login and REST credential block, and the exact `inpersonpayments.wpcomstaging.com` host. The +runner acquires a REST-backed store lock before any ADB interaction and removes it on exit. + +## Local Setup + +Install prerequisites: + +```bash +source .maestro/scripts/configure-toolchain.sh +adb devices +``` + +The script selects an installed JDK 21, downloads the immutable Maestro 2.8.0 +release archive into the workspace, verifies the SHA-256 in +`toolchain.properties`, and runs the checker. The runner, doctor, and CI fail +fast when either version differs; Buildkite uses the same path before building. + +Create local credentials: + +```bash +cp .maestro/env.example .maestro/.env.local +``` + +Fill `.maestro/.env.local` yourself from the canonical secret store. Do not paste credential values into agent conversations. +Validate the file before running flows, especially after pasting passwords: + +```bash +.maestro/scripts/lint-env.py +``` + +Run the pre-flight doctor when setting up a machine, changing credentials, or preparing CI secrets: + +```bash +.maestro/scripts/doctor.sh --profile phone-full --store lab --device emulator-5554 +``` + +### Store data prerequisites + +`orders_create` selects an existing live-store customer and edits only the customer copy attached to the order draft. +The app creates that `Order.Customer` in `OrderCreateEditCustomerAddFragment` and +`OrderCreateEditViewModel.onCustomerEdited` replaces only `orderDraft.customer`; it does not update the store customer. +The flow captures the selected email, verifies it on the draft, verifies the edited marker on the persisted order, +then searches the customer list again and requires the original email to be unchanged. The configured store must have +at least two existing customers with email addresses; missing data fails as an explicit prerequisite. + +## Running + +Default local run: lab store, `smoke_core` only, quarantine excluded. + +```bash +.maestro/scripts/run-smoke-tests.sh --store lab +``` + +Common variants: + +```bash +.maestro/scripts/run-smoke-tests.sh --profile core +.maestro/scripts/run-smoke-tests.sh --plan --profile phone-full +.maestro/scripts/run-smoke-tests.sh --profile phone-full --device emulator-5554 +.maestro/scripts/run-smoke-tests.sh --profile release +.maestro/scripts/run-smoke-tests.sh --profile burst +.maestro/scripts/run-smoke-tests.sh --profile pos-tablet --device Pixel_Tablet_API_35 +.maestro/scripts/run-smoke-tests.sh --profile android-system --device Pixel_8_API_35 +.maestro/scripts/doctor.sh --profile phone-full --store lab +.maestro/scripts/run-smoke-tests.sh --device emulator-5554 +.maestro/scripts/run-smoke-tests.sh --apk WooCommerce/build/outputs/apk/wasabi/debug/WooCommerce-wasabi-debug.apk +.maestro/scripts/run-smoke-tests.sh --include-tags smoke_extended --include-quarantine --store lab +.maestro/scripts/run-smoke-tests.sh --include-tags flaky_quarantine .maestro/flows/orders_create.yaml +.maestro/scripts/run-smoke-tests.sh --store shared --include-tags smoke_core +.maestro/scripts/run-smoke-tests.sh --repeat 3 --store lab --include-tags smoke_core +.maestro/scripts/run-smoke-tests.sh --rerun-failed ~/woocommerce-maestro-output/20260708141815/report.xml --store lab +``` + +Profiles are copy/paste-safe presets: + +- `core`: lab store, `smoke_core`, quarantine and Android system surfaces excluded. +- `phone-full`: lab store, `smoke_core,smoke_extended`, tablet POS and Android system surfaces excluded. This includes quarantined phone flows. +- `release`: shared store, `smoke_core,smoke_extended,destructive`, quarantine, tablet POS, and Android system surfaces excluded. +- `burst`: same as `release`, repeated 3 times. +- `pos-tablet`: lab store, `pos_tablet`, quarantine included. +- `android-system`: lab store, `android_system`, quarantine included. Requires an English Pixel Launcher AVD with the + Wasabi app discoverable as `Woo (Dev)` in the app drawer. + +Use `--plan` with a profile or tag selection to print the exact store, repeat count, filters, and ordered flow list. +Planning is side-effect-free: it does not load credentials, create output directories, call Maestro/ADB, or acquire a +store lock. `flaky_quarantine` stays excluded unless the selected profile includes it or `--include-quarantine` is +passed explicitly. A zero-flow selection is an error in both the runner and doctor. + +`--rerun-failed report.xml` reads failed/flaky JUnit test cases and runs only those flow files. It still honors +store, device, APK, repeat, and profile options. + +The runner: + +- selects one connected device automatically, or prompts when several are attached; +- captures and restores animation settings; +- can seed deterministic fixtures through the WooCommerce REST API when `--seed` is used; +- writes created entity IDs to `run-manifest.json` when seeding; +- deletes exactly those manifest IDs during cleanup when seeding; +- performs a guarded stale-orphan sweep for `SUITE--` entities older than 48h when seeding; +- retries each failed non-destructive flow once and records pass-on-retry as flaky; +- never blindly retries a failed destructive mutation; cleanup runs first and the failure remains visible; +- redacts `MAESTRO_WOO_*` values from logs; +- stores artifacts outside the repo under `$HOME/woocommerce-maestro-output//`; +- writes copy/paste commands into the HTML report for rerunning the same selection, rerunning failed flows, and running + the doctor. + +## Tags + +- `smoke_core`: stable non-destructive release signal paths. +- `smoke_extended`: broader P2 coverage. +- `pos_tablet`: POS flows, tablet AVD required. +- `android_system`: launcher/system-surface flows, English Pixel Launcher AVD required. +- `system_surface`: flow enters Android-owned UI; assertions stop at the documented handoff boundary. +- `destructive`: mutates store data. +- `flaky_quarantine`: provisional or unstable flows excluded from real runs. + +The imported PR #15413 flows outside the four core paths are intentionally tagged `flaky_quarantine` until they graduate through the burst-based promotion policy. + +## Coverage + +Traceability is committed in `.maestro/smoke-coverage.yaml`. Each flow declares covered checklist items in a `# p2:` header. + +Validate offline: + +```bash +.maestro/scripts/check-smoke-coverage.py +``` + +Regenerate strings env after copy changes: + +```bash +.maestro/scripts/generate-strings-env.py --check-flow-references +``` + +## Documentation + +The self-contained system guide lives at `.maestro/docs/index.html`. diff --git a/.maestro/config.yaml b/.maestro/config.yaml new file mode 100644 index 000000000000..194432c87c25 --- /dev/null +++ b/.maestro/config.yaml @@ -0,0 +1,85 @@ +# Maestro workspace configuration for WooCommerce Android smoke tests. +# +# Execution order mirrors the Smoke Testing P2 category order: +# Login → Dashboard → Orders → Products → Hub Menu → +# Blaze → Google for Woo → POS +# +# Within the Login group, login_successful is intentionally LAST so the +# app ends the login sequence authenticated with the primary Woo account +# (MAESTRO_WOO_WPCOM_EMAIL / MAESTRO_WOO_JETPACK_STORE_URL). +# Subsequent flows reuse that session via subflows/ensure_logged_in.yaml, avoiding the WP.com +# security screens that fire when the same account re-authenticates too +# many times in quick succession. +# +# Tags follow the v2 smoke-test taxonomy: +# smoke_core Stable non-destructive release-signal paths. +# smoke_extended Broader P2 coverage. +# pos_tablet POS flows, tablet AVD required. +# destructive Mutates store data; shared store only in CI under lock. +# flaky_quarantine Provisional or known-unstable flows excluded by runner +# defaults until promotion criteria are met. +# +# The local runner `.maestro/scripts/run-smoke-tests.sh` owns default tag +# filtering, store selection, fixture seed/cleanup, retry accounting, and +# artifact policy. This config keeps direct `maestro test .maestro/` +# invocations ordered consistently. +# +# P2 reference: +# https://woomobilep2.wordpress.com/flows-for-app-features-smoke-testing/ + +appId: com.woocommerce.android.dev + +flows: + - "flows/*" + +executionOrder: + continueOnFailure: true + flowsOrder: + # ── Login ──────────────────────────────────────────────────────────── + # Error-path logins first (each clearState + re-authenticates); + # login_successful runs last so the app is left authenticated. + - flows/login_not_wp_site.yaml + - flows/login_wrong_credentials.yaml + - flows/login_help.yaml + - flows/login_not_woo_store.yaml + - flows/login_wrong_account.yaml + - flows/login_no_jetpack.yaml + - flows/login_google.yaml + - flows/login_successful.yaml + + # ── Dashboard / Stats ─────────────────────────────────────────────── + - flows/dashboard_stats.yaml + - flows/dashboard_view_all_analytics.yaml + - flows/dashboard_customize.yaml + + # ── Orders ─────────────────────────────────────────────────────────── + - flows/orders_list_and_search.yaml + - flows/orders_create.yaml + - flows/orders_details_and_actions.yaml + - flows/orders_mark_complete.yaml + - flows/orders_cash_payment.yaml + - flows/orders_refund.yaml + + # ── Products ───────────────────────────────────────────────────────── + - flows/products_list_and_sort.yaml + - flows/products_detail.yaml + - flows/products_variations_and_tags.yaml + - flows/products_create.yaml + - flows/products_media_upload.yaml + + # ── Hub Menu ──────────────────────────────────────────────────────── + - flows/hub_menu_settings.yaml + - flows/hub_menu_payments.yaml + - flows/hub_menu_coupons.yaml + - flows/hub_menu_customers_inbox.yaml + - flows/hub_menu_admin_and_store.yaml + + # ── Blaze ──────────────────────────────────────────────────────────── + - flows/blaze_campaign.yaml + + # ── Google for Woo ────────────────────────────────────────────────── + - flows/google_for_woo.yaml + + # ── POS (tablet only) ──────────────────────────────────────────────── + - flows/pos_search_and_coupons.yaml + - flows/pos_cash_payment.yaml diff --git a/.maestro/docs/index.html b/.maestro/docs/index.html new file mode 100644 index 000000000000..78bf0d4957c6 --- /dev/null +++ b/.maestro/docs/index.html @@ -0,0 +1,74 @@ + + + + + WooCommerce Android Maestro Smoke System + + + +

WooCommerce Android Maestro Smoke System

+

Self-contained guide for the Maestro smoke suite rooted at .maestro/.

+ +

Architecture

+

Top-level flows live in .maestro/flows/. Reusable login and navigation helpers live in .maestro/subflows/. The runner owns device selection, store selection, fixture seeding, cleanup, retries, artifacts, and reports.

+
flows -> subflows -> run-smoke-tests.sh -> Maestro CLI/MCP -> reports
+ +

Stores

+ + + + + + +
StorePurposeRules
labDisposable Jurassic Ninja automation store.Default for local runs, repair loops, destructive iteration, and can-fail checks.
sharedinpersonpayments.wpcomstaging.com.Release-tool and Thursday burst runs. Destructive tags are CI-only and require seed, scoped REST credentials, the exact host, and a pre-device REST lock.
+ +

Local Runs

+
cp .maestro/env.example .maestro/.env.local
+.maestro/scripts/run-smoke-tests.sh --store lab
+.maestro/scripts/run-smoke-tests.sh --plan --profile phone-full
+.maestro/scripts/run-smoke-tests.sh --device emulator-5554 --store lab
+.maestro/scripts/run-smoke-tests.sh --profile android-system --device Pixel_8_API_35
+.maestro/scripts/run-smoke-tests.sh --include-tags smoke_extended --include-quarantine --store lab
+.maestro/scripts/run-smoke-tests.sh --repeat 3 .maestro/flows/orders_list_and_search.yaml
+ +

Fixtures and Cleanup

+

The runner calls seed-fixtures.py before flows. It creates pending orders, refundable orders, a variable product with variations and tags, a simple product, a coupon, and a customer. Consumable fixtures are created twice.

+

Every created entity is written to run-manifest.json. Cleanup deletes exactly those IDs. A guarded stale-orphan sweep only matches strict SUITE-<date>-<hash> values older than 48 hours and logs deletions into the run report.

+ +

Tags

+ + + + + + + + + + + +
TagMeaning
smoke_coreStable non-destructive critical paths. Default release signal.
smoke_extendedBroader P2 coverage.
pos_tabletPOS tablet flows.
android_systemLauncher/system-surface flows on an English Pixel Launcher AVD.
system_surfaceFlows that enter Android-owned UI and stop at the documented handoff boundary.
destructiveMutates store data.
flaky_quarantineExcluded from real runs until promotion.
+ +

Selector Rules

+

Interact through id: selectors whenever Compose testTag is available. Assert user-visible text through generated values from .maestro/strings.env. Use regex variables for parameterized strings and manifest-derived env vars for exact fixture values.

+

point: selectors require an inline YAML comment explaining why no semantic selector exists.

+ +

Reliability Policy

+

Each failed flow gets one automatic retry. Pass-on-retry and pass-with-recovery count as flaky and make the run non-clean. Promotion requires clean results across two consecutive bi-weekly Thursday bursts. Quarantine is triggered by two flaky results in one burst or flaky results in two consecutive bursts.

+ +

Coverage

+

The committed snapshot is .maestro/smoke-coverage.yaml. Flow headers declare checklist IDs with # p2:. Validate locally:

+
.maestro/scripts/check-smoke-coverage.py
+.maestro/scripts/generate-strings-env.py --check-flow-references
+ + diff --git a/.maestro/env.example b/.maestro/env.example new file mode 100644 index 000000000000..f15310bfd0df --- /dev/null +++ b/.maestro/env.example @@ -0,0 +1,112 @@ +# ───────────────────────────────────────────────────────────────────────── +# WooCommerce Android — Maestro smoke-test environment variables +# ───────────────────────────────────────────────────────────────────────── +# +# The runner passes MAESTRO_* values to Maestro with the MAESTRO_ prefix +# stripped, so MAESTRO_WOO_WPCOM_EMAIL is referenced inside YAML as ${WOO_WPCOM_EMAIL}. +# +# Setup: +# 1. Copy this file: cp .maestro/env.example .maestro/.env.local +# 2. Fill in values (see secret-store links below). +# 3. Load before running flows: +# set -a && source .maestro/.env.local && set +a +# .maestro/scripts/run-smoke-tests.sh +# +# Or pass inline: +# maestro test -e WOO_WPCOM_EMAIL=… -e WOO_WPCOM_PASSWORD=… .maestro/flows/ +# +# P2 reference (login scenarios + test store mapping): +# https://woomobilep2.wordpress.com/flows-for-app-features-smoke-testing/ +# +# NEVER commit filled-in credentials. .env.local is git-ignored. +# Credentials should be copied directly by a human from the canonical secret +# store into .maestro/.env.local or Buildkite secrets. Do not paste them into +# agent conversations. + + +# ═════════════════════════════════════════════════════════════════════════ +# REQUIRED STORE BLOCKS +# ═════════════════════════════════════════════════════════════════════════ +# The developer CLI defaults to --store lab. Shared-store runs require +# --store shared and are intended for release-tool and Thursday burst usage. +# +# Each store block needs: +# *_JETPACK_STORE_URL Jetpack-connected WooCommerce site root URL, not /wp-admin/ +# *_WPCOM_EMAIL WP.com login email for the selected store +# *_WPCOM_PASSWORD WP.com password for the selected store +# Seeding additionally needs: +# *_CONSUMER_KEY WooCommerce REST API consumer key +# *_CONSUMER_SECRET WooCommerce REST API consumer secret +# +# The runner copies the selected store block into the generic variables used +# by YAML. + +# Lab store: automation-owned WooCommerce store connected to Jetpack/WP.com. +# Use this for the broad smoke suite. Do not point this block at the +# no-Jetpack Jurassic Ninja site used by login_no_jetpack.yaml. +MAESTRO_WOO_LAB_JETPACK_STORE_URL= +MAESTRO_WOO_LAB_WPCOM_EMAIL= +MAESTRO_WOO_LAB_WPCOM_PASSWORD= +MAESTRO_WOO_LAB_CONSUMER_KEY= +MAESTRO_WOO_LAB_CONSUMER_SECRET= + +# Shared store: long-lived staging store used by release signal runs. +MAESTRO_WOO_SHARED_JETPACK_STORE_URL=https://inpersonpayments.wpcomstaging.com/ +MAESTRO_WOO_SHARED_WPCOM_EMAIL= +MAESTRO_WOO_SHARED_WPCOM_PASSWORD= +MAESTRO_WOO_SHARED_CONSUMER_KEY= +MAESTRO_WOO_SHARED_CONSUMER_SECRET= + + +# ═════════════════════════════════════════════════════════════════════════ +# OPTIONAL FLOW-SPECIFIC VARIABLES +# ═════════════════════════════════════════════════════════════════════════ +# Names match the iOS credential-setup page structure so both platforms +# stay aligned. Leave commented-out until the matching flow is added. + +# ── P2: Login > Not a Woo store (login_not_woo_store.yaml) ────────────── +# Requires a WordPress site without WooCommerce and its site-admin +# credentials. The WP.com pair is an optional fallback for sites routed +# through WordPress.com authentication; set both values or leave both blank. +MAESTRO_WOO_NOT_A_WOO_STORE_URL=https://notawoostore.wordpress.com/ +MAESTRO_WOO_NOT_A_WOO_STORE_SITE_ADMIN_USERNAME= +MAESTRO_WOO_NOT_A_WOO_STORE_SITE_ADMIN_PASSWORD= +# Optional WP.com fallback. Set both values or leave both blank. +MAESTRO_WOO_NOT_A_WOO_STORE_WPCOM_EMAIL= +MAESTRO_WOO_NOT_A_WOO_STORE_WPCOM_PASSWORD= + +# ── P2: Login > Wrong account for the store (login_wrong_account.yaml) ── +# Reuses the selected *_WPCOM_EMAIL + *_WPCOM_PASSWORD — only the +# store URL differs. The primary account intentionally does NOT have +# access to this store. +MAESTRO_WOO_WRONG_ACCOUNT_STORE_URL=https://site-for-woocommerce12a3fasdf45dfs6789.mystagingwebsite.com/ + +# ── P2: Login > No Jetpack (login_no_jetpack.yaml) ────────────────────── +# Requires a Jurassic Ninja site with WooCommerce installed but NO +# Jetpack. Login uses site credentials (not WP.com). JN provisions a +# fresh admin user per site — copy the credentials JN hands back. +# Keep these separate from MAESTRO_WOO_*_JETPACK_STORE_URL and +# MAESTRO_WOO_*_WPCOM_*. +MAESTRO_WOO_NO_JETPACK_SITE_URL= +MAESTRO_WOO_NO_JETPACK_SITE_ADMIN_USERNAME= +MAESTRO_WOO_NO_JETPACK_SITE_ADMIN_PASSWORD= + +# ── P2: Login > Passwordless login (WordPress.com passwordless account) ─ +# Separate WP.com account (no password — magic-link only). Reading the +# magic link requires the Mailosaur API key. +# Account credentials and Mailosaur API key live in the canonical secret store. +# MAESTRO_WOO_PASSWORDLESS_STORE_URL=https://woomobilepasswordlesslogin.wpcomstaging.com/ +# MAESTRO_WOO_PASSWORDLESS_EMAIL=woomobile@bakbmdyy.mailosaur.net +# MAESTRO_WOO_PASSWORDLESS_PASSWORD= +# MAESTRO_MAILOSAUR_API_KEY= + +# ── P2: Login > Login with 2FA ────────────────────────────────────────── +# Use a dedicated test WP.com account with 2FA temporarily enabled; the +# 2FA code must be generated at run time (authenticator seed or TOTP lib). +# MAESTRO_WOO_2FA_EMAIL= +# MAESTRO_WOO_2FA_PASSWORD= +# MAESTRO_WOO_2FA_STORE_URL= +# MAESTRO_WOO_2FA_TOTP_SECRET= + +# ── P2: Login > Social login (Apple/Google) ───────────────────────────── +# Uses device-level Google/Apple accounts — no Maestro env vars needed. diff --git a/.maestro/flows/android_quick_actions.yaml b/.maestro/flows/android_quick_actions.yaml new file mode 100644 index 000000000000..2d378122f29f --- /dev/null +++ b/.maestro/flows/android_quick_actions.yaml @@ -0,0 +1,103 @@ +# Smoke Test: Android system - App shortcuts +# p2: other.quick-actions +# P2 ref: Other > Quick Actions (long press on app icon) +# +# Verifies on the real launcher surface: +# - Long-pressing Woo (Dev) exposes Create order and Payments +# - Create order opens a fresh order draft +# - Payments opens the Payments hub +# +# Run only through the android-system profile on an English Pixel Launcher AVD. +appId: com.woocommerce.android.dev +name: "Android system - App shortcuts" +tags: + - android_system + - flaky_quarantine +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +# Dynamic shortcuts are registered after a site is selected. Stop the app and +# exercise them from Pixel Launcher's app drawer rather than launching an +# equivalent intent directly. +- stopApp +- pressKey: HOME +- swipe: + direction: UP + duration: 500 + +- extendedWaitUntil: + visible: "Search apps" + timeout: 10000 + label: "Open Pixel Launcher app drawer" + +- tapOn: + text: "Search apps" + label: "Focus app search" +- inputText: "Woo" +- hideKeyboard + +- extendedWaitUntil: + visible: "Woo \\(Dev\\)" + timeout: 10000 + label: "Find the Wasabi app icon" + +- longPressOn: + text: "Woo \\(Dev\\)" + label: "Open Woo app shortcuts" + +- assertVisible: + text: "Create order" + label: "Create order shortcut is present" +- assertVisible: + text: "Payments" + label: "Payments shortcut is present" + +- takeScreenshot: android_quick_actions + +- tapOn: + text: "Create order" + label: "Launch Create order shortcut" + +- extendedWaitUntil: + visible: "New order" + timeout: 30000 + label: "Create order shortcut opens an order draft" +- assertVisible: "Add products" +- takeScreenshot: android_quick_action_create_order + +- back +- runFlow: + when: + visible: "Discard" + commands: + - tapOn: + text: "Discard" + label: "Discard shortcut-created order draft" + +# Reopen the launcher shortcuts and verify the Payments destination too. +- stopApp +- pressKey: HOME +- swipe: + direction: UP + duration: 500 + +- extendedWaitUntil: + visible: "Search apps" + timeout: 10000 +- tapOn: "Search apps" +- inputText: "Woo" +- hideKeyboard +- extendedWaitUntil: + visible: "Woo \\(Dev\\)" + timeout: 10000 +- longPressOn: "Woo \\(Dev\\)" +- tapOn: + text: "Payments" + label: "Launch Payments shortcut" + +- extendedWaitUntil: + visible: "Pay In Person" + timeout: 30000 + label: "Payments shortcut opens the Payments hub" +- takeScreenshot: android_quick_action_payments diff --git a/.maestro/flows/blaze_campaign.yaml b/.maestro/flows/blaze_campaign.yaml new file mode 100644 index 000000000000..540a42e6f66d --- /dev/null +++ b/.maestro/flows/blaze_campaign.yaml @@ -0,0 +1,85 @@ +# Smoke Test: Blaze - Campaign entry point +# P2 probe: hub.blaze.create (not counted when the store hides Blaze) +# P2 ref: Hub menu > Blaze > Create Campaign +# +# Tapping "Blaze" in the More Menu has three possible destinations +# depending on store state (see onPromoteProductsWithBlaze in +# MoreMenuViewModel → BlazeCampaignCreationDispatcher): +# +# 1. Store has >=1 campaign already → Blaze campaign list screen +# ("Blaze campaigns" title, blaze_campaign_list_title). +# 2. Store has no prior campaign → BlazeCampaignCreationIntro screen +# ("Get your products seen by millions" + "Start your campaign", +# blaze_campaign_creation_new_intro_*). +# 3. Store has a recent campaign + products → creation form / +# product selector. +# +# Smoke-level guarantee: when the store exposes the Blaze entry point, +# tapping it leaves the More Menu and lands on one of the Blaze screens +# above. Some lab stores do not expose Blaze in the More Menu at all; +# in that case the flow records the unavailable state instead of +# failing on a feature-gated menu item. +appId: com.woocommerce.android.dev +name: "Blaze - Campaign creation" +tags: + - smoke_extended + - flaky_quarantine + - hub_menu +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/navigate_to_more_menu.yaml + +# ── Find Blaze if this store exposes it ────────────────────────────── +# More Menu is a Compose scroll surface; a couple of plain scrolls are +# enough to reach the bottom on phone viewports without failing when +# the list is already fully visible. +- scroll +- scroll + +- runFlow: + when: + visible: "Blaze" + commands: + - tapOn: + text: "Blaze" + retryTapIfNoChange: true + label: "Tap Blaze menu item" + + # The click loads campaigns from the network first (async), so it can + # take a few seconds before the destination fragment actually renders. + - extendedWaitUntil: + notVisible: + id: "more_menu_compose_view" + timeout: 25000 + label: "Verify we left the More Menu after tap" + + # Match any of the per-destination anchor strings. Regex OR so the + # flow passes regardless of the store's current campaign/product + # state. + - extendedWaitUntil: + visible: ".*Blaze campaigns.*|.*Get your products seen.*|.*Start your campaign.*|.*Select products.*|.*Campaign preview.*" + timeout: 20000 + label: "Wait for Blaze destination screen" + + - takeScreenshot: blaze_screen + + # The intro screen's top-left control is a close ("X") icon; the list + # screen has a back arrow. Both call onDismiss / pop, so a single + # `back` returns to the More Menu in both cases. + - back + + - extendedWaitUntil: + visible: + id: "more_menu_compose_view" + timeout: 15000 + label: "Back on More Menu" + +- runFlow: + when: + visible: + id: "more_menu_compose_view" + commands: + - takeScreenshot: blaze_more_menu_state diff --git a/.maestro/flows/dashboard_customize.yaml b/.maestro/flows/dashboard_customize.yaml new file mode 100644 index 000000000000..4597657204bd --- /dev/null +++ b/.maestro/flows/dashboard_customize.yaml @@ -0,0 +1,212 @@ +# Smoke Test: Dashboard - Customize all cards +# p2: dashboard.customization +# P2 ref: Dashboard/Stats > Customization — all cards can be toggled + reordered +# +# Verifies: +# - Every supported dashboard card is classified as selected, unselected, +# unavailable, or feature-gated hidden. +# - Every available card is toggled and left selected before saving. +# - Selection and a first-pair reorder persist across an app relaunch. +# - The exact original selection and order are restored and verified after +# a second relaunch, so this flow does not leave shared dashboard state. +appId: com.woocommerce.android.dev +name: "Dashboard - Customize all cards" +tags: + - smoke_extended + - flaky_quarantine + - dashboard +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/open_dashboard_widget_editor.yaml + +- evalScript: ${output.dashboardWidgets = ['ai_assistant', 'push_notifications', 'store_setup', 'performance', 'top_performers', 'blaze', 'inbox', 'reviews', 'coupons', 'stock', 'orders', 'google_ads']} +- evalScript: ${output.dashboardOriginalStates = {}} + +# Record the exact initial state before changing anything. Hidden cards are +# included in the editor's state tag even though they are not rendered as rows. +- evalScript: ${output.dashboardWidgetIndex = 0} +- repeat: + while: + true: ${output.dashboardWidgetIndex < output.dashboardWidgets.length} + commands: + - runFlow: + file: ../subflows/dashboard_record_widget_state.yaml + env: + CARD_SLUG: ${output.dashboardWidgets[output.dashboardWidgetIndex]} + - evalScript: ${output.dashboardWidgetIndex++} + +# Select every available card that was initially unselected. This first pass +# guarantees more than one selected card before the deselect/reselect pass, +# respecting the editor's invariant that the final selected card is disabled. +- evalScript: ${output.dashboardWidgetIndex = 0} +- repeat: + while: + true: ${output.dashboardWidgetIndex < output.dashboardWidgets.length} + commands: + - runFlow: + file: ../subflows/dashboard_select_widget.yaml + env: + CARD_SLUG: ${output.dashboardWidgets[output.dashboardWidgetIndex]} + - evalScript: ${output.dashboardWidgetIndex++} + +# Exercise both transitions for cards that were already selected. Together, +# the two passes toggle every available card and normalize all of them selected. +- evalScript: ${output.dashboardWidgetIndex = 0} +- repeat: + while: + true: ${output.dashboardWidgetIndex < output.dashboardWidgets.length} + commands: + - runFlow: + file: ../subflows/dashboard_exercise_selected_widget.yaml + env: + CARD_SLUG: ${output.dashboardWidgets[output.dashboardWidgetIndex]} + - evalScript: ${output.dashboardWidgetIndex++} + +# Capture the first two available rows by their stable slugs, swap them, and +# prove the editor changed without relying on localized card titles. +- scrollUntilVisible: + element: + id: "dashboard_widget_editor_row_0_.*" + direction: UP + timeout: 10000 + label: "Return to first dashboard card" + +- evalScript: ${output.dashboardOriginalOrder = {}} +- evalScript: ${output.dashboardWidgetIndex = 0} +- repeat: + while: + true: ${output.dashboardWidgetIndex < output.dashboardWidgets.length} + commands: + - runFlow: + file: ../subflows/dashboard_record_widget_order.yaml + env: + CARD_SLUG: ${output.dashboardWidgets[output.dashboardWidgetIndex]} + - evalScript: ${output.dashboardWidgetIndex++} +- evalScript: ${output.dashboardOriginalFirst = output.dashboardOriginalOrder[0]} +- evalScript: ${output.dashboardOriginalSecond = output.dashboardOriginalOrder[1]} +- assertTrue: + condition: ${output.dashboardOriginalFirst.length > 0 && output.dashboardOriginalSecond.length > 0 && output.dashboardOriginalFirst != output.dashboardOriginalSecond} + label: "Capture two distinct dashboard cards" + +- swipe: + from: + id: "dashboard_widget_editor_drag_handle_1_.*" + direction: UP + duration: 1000 + label: "Move second dashboard card above first" + +- waitForAnimationToEnd +- assertVisible: + id: "dashboard_widget_editor_row_0_${output.dashboardOriginalSecond}_.*" + label: "Verify second dashboard card moved above first" + +- tapOn: + text: "SAVE" + label: "Save dashboard card selection and order" + +- extendedWaitUntil: + visible: + id: "dashboard_container" + timeout: 15000 + +# Relaunch and reopen to prove the persisted state, rather than asserting only +# the in-memory editor model. +- stopApp +- launchApp +- runFlow: + file: ../subflows/open_dashboard_widget_editor.yaml + +- evalScript: ${output.dashboardWidgetIndex = 0} +- repeat: + while: + true: ${output.dashboardWidgetIndex < output.dashboardWidgets.length} + commands: + - runFlow: + file: ../subflows/dashboard_assert_widget_selected.yaml + env: + CARD_SLUG: ${output.dashboardWidgets[output.dashboardWidgetIndex]} + - evalScript: ${output.dashboardWidgetIndex++} + +- assertVisible: + id: "dashboard_widget_editor_row_0_${output.dashboardOriginalSecond}_.*" + label: "Verify dashboard reorder persisted across relaunch" +- assertVisible: + id: "dashboard_widget_editor_row_1_${output.dashboardOriginalFirst}_.*" + label: "Verify original first dashboard card persisted second" + +- takeScreenshot: dashboard_customize_persisted + +# Restore exact selection state, then swap the first pair back. +- evalScript: ${output.dashboardWidgetIndex = 0} +- repeat: + while: + true: ${output.dashboardWidgetIndex < output.dashboardWidgets.length} + commands: + - runFlow: + file: ../subflows/dashboard_restore_widget.yaml + env: + CARD_SLUG: ${output.dashboardWidgets[output.dashboardWidgetIndex]} + - evalScript: ${output.dashboardWidgetIndex++} + +- scrollUntilVisible: + element: + id: "dashboard_widget_editor_row_0_.*" + direction: UP + timeout: 10000 + +- swipe: + from: + id: "dashboard_widget_editor_drag_handle_1_.*" + direction: UP + duration: 1000 + label: "Restore original dashboard card order" + +- waitForAnimationToEnd +- assertVisible: + id: "dashboard_widget_editor_row_0_${output.dashboardOriginalFirst}_.*" + label: "Verify original dashboard card order before save" + +- tapOn: + text: "SAVE" + label: "Save restored dashboard state" + +- extendedWaitUntil: + visible: + id: "dashboard_container" + timeout: 15000 + +# Verify restoration itself persisted, so a passing run leaves the shared site +# exactly as it found it. +- stopApp +- launchApp +- runFlow: + file: ../subflows/open_dashboard_widget_editor.yaml + +- evalScript: ${output.dashboardWidgetIndex = 0} +- repeat: + while: + true: ${output.dashboardWidgetIndex < output.dashboardWidgets.length} + commands: + - runFlow: + file: ../subflows/dashboard_assert_widget_restored.yaml + env: + CARD_SLUG: ${output.dashboardWidgets[output.dashboardWidgetIndex]} + - evalScript: ${output.dashboardWidgetIndex++} + +- assertVisible: + id: "dashboard_widget_editor_row_0_${output.dashboardOriginalFirst}_.*" + label: "Verify restored first dashboard card persisted" +- assertVisible: + id: "dashboard_widget_editor_row_1_${output.dashboardOriginalSecond}_.*" + label: "Verify restored second dashboard card persisted" + +- takeScreenshot: dashboard_customize_restored + +- back +- extendedWaitUntil: + visible: + id: "dashboard_container" + timeout: 10000 diff --git a/.maestro/flows/dashboard_stats.yaml b/.maestro/flows/dashboard_stats.yaml new file mode 100644 index 000000000000..8be8aae34cd4 --- /dev/null +++ b/.maestro/flows/dashboard_stats.yaml @@ -0,0 +1,112 @@ +# Smoke Test: Dashboard / Stats +# p2: dashboard.stats +# P2 ref: Dashboard/Stats > Charts respond, View All analytics, Customization +# +# Verifies: +# - Dashboard loads with stats cards +# - Revenue, Orders, Visitors, Conversion stats are visible +# - Date range switching works (Today, This Week, This Month, This Year) +# - Top performers card is visible +# - "View All" store analytics navigation +appId: com.woocommerce.android.dev +name: "Dashboard - Stats and analytics" +tags: + - smoke_core + - dashboard +--- +# Reuse the logged-in session — login flows run first and leave the +# app authenticated. See subflows/ensure_logged_in.yaml. +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +# Verify dashboard container loads +- assertVisible: + id: "dashboard_container" + label: "Dashboard container is visible" + +# Verify stats card via Compose testTag (DashboardStatsTestTags.DASHBOARD_STATS_CARD) +- scrollUntilVisible: + element: + id: "dashboard_stats_card" + direction: DOWN + timeout: 15000 + label: "Scroll to stats card" + +- assertVisible: + id: "dashboard_stats_card" + label: "Stats card is visible" + +# Verify at least one stat-tile label is rendered on the card. +# +# The card's inner layout is a legacy AndroidView (DashboardStatsView) +# with four side-by-side tiles. Their labels come from: +# - R.string.analytics_total_sales_title → "Total sales" +# (was "Revenue" on older builds — fall back to that spelling) +# - R.string.dashboard_stats_orders → "Orders" +# - dashboard_stats_visitors → "Visitors" +# - dashboard_stats_conversion → "Conversion" +# +# Labels render immediately after the card inflates — only the tile +# *values* show the skeleton em-dash ("—") while the Analytics API +# request is in flight. Matching any one of the labels is the most +# stable signal that the card rendered with its tiles, and it's +# robust against future string renames. +- assertVisible: + text: ".*Total sales.*|.*Revenue.*|.*Orders.*|.*Visitors.*" + label: "At least one stats tile label is visible" + +- takeScreenshot: dashboard_stats_overview + +# Test date range switching via Compose testTag (DashboardStatsTestTags.STATS_RANGE_DROPDOWN_BUTTON) +- tapOn: + id: "stats_range_dropdown_button" + label: "Tap date range dropdown" + +# The DropdownMenu is a Compose Popup, so assert its stable item text rather +# than relying on its test tag surfacing in Maestro's hierarchy. This is +# mandatory: otherwise the flow could pass without changing the chart range. +- extendedWaitUntil: + visible: "This Week" + timeout: 10000 + label: "Wait for dashboard date range menu" + +- tapOn: + text: "This Week" + retryTapIfNoChange: true + label: "Select This Week range" + +- extendedWaitUntil: + visible: + id: "dashboard_stats_card" + timeout: 10000 + label: "Wait for weekly dashboard stats" + +- assertNotVisible: + text: "Custom" + label: "Date range menu closes after selection" + +- takeScreenshot: dashboard_stats_week + +# Scroll down to top performers via Compose testTag (DashboardStatsTestTags.DASHBOARD_TOP_PERFORMERS_CARD) +- scrollUntilVisible: + element: + id: "dashboard_top_performers_card" + direction: DOWN + timeout: 15000 + label: "Scroll to top performers card" + +- assertVisible: + id: "dashboard_top_performers_card" + label: "Top performers card is visible" + +- takeScreenshot: dashboard_top_performers + +# Scroll back up to the top of the dashboard. +# `swipe: direction: DOWN` swipes the finger downward, which scrolls +# the list content upward. +- swipe: + direction: DOWN + duration: 500 +- swipe: + direction: DOWN + duration: 500 diff --git a/.maestro/flows/dashboard_view_all_analytics.yaml b/.maestro/flows/dashboard_view_all_analytics.yaml new file mode 100644 index 000000000000..583f14f9e59b --- /dev/null +++ b/.maestro/flows/dashboard_view_all_analytics.yaml @@ -0,0 +1,72 @@ +# Smoke Test: Dashboard / View All store analytics +# p2: dashboard.analytics +# P2 ref: Dashboard/Stats > "View All" store analytics +# +# Verifies: +# - Tapping "View all store analytics" on the Top Performers card +# opens the Analytics Hub screen +# - The hub renders its date selector and at least one analytics card +# (Revenue, Orders, etc.) +appId: com.woocommerce.android.dev +name: "Dashboard - View All store analytics" +tags: + - smoke_extended + - flaky_quarantine + - dashboard +--- +# Reuse the logged-in session — login flows run first and leave the +# app authenticated. See subflows/ensure_logged_in.yaml. +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +# Make sure the dashboard is actually on screen before scrolling +- assertVisible: + id: "dashboard_container" + label: "Dashboard container is visible" + +# Scroll the Top Performers card into view — the "View all store analytics" +# link lives at the bottom of that card +- scrollUntilVisible: + element: + id: "dashboard_top_performers_card" + direction: DOWN + timeout: 15000 + label: "Scroll to Top Performers card" + +# The WCTextButton's label comes from R.string.analytics_section_see_all +# ("View all store analytics"). Match loosely so casing tweaks don't +# break the flow. +- scrollUntilVisible: + element: + text: ".*(?i)view all store analytics.*" + direction: DOWN + timeout: 10000 + label: "Scroll to View All store analytics button" + +- tapOn: + text: ".*(?i)view all store analytics.*" + label: "Tap View all store analytics" + +# Analytics Hub screen uses View-based XML (fragment_analytics.xml), +# so Maestro can find the root IDs directly. +- extendedWaitUntil: + visible: + id: "analyticsRefreshLayout" + timeout: 20000 + +- assertVisible: + id: "analyticsDateSelectorCard" + label: "Analytics Hub date range card is visible" + +- assertVisible: + id: "cards" + label: "Analytics Hub cards list is visible" + +# At least one of the hub's canonical cards should be on screen. +# Revenue is the first one and always rendered; fall back to Orders in +# case layout/ordering changes in the future. +- assertVisible: + text: ".*Revenue.*|.*Orders.*" + label: "Revenue or Orders analytics card is visible" + +- takeScreenshot: dashboard_view_all_analytics diff --git a/.maestro/flows/google_for_woo.yaml b/.maestro/flows/google_for_woo.yaml new file mode 100644 index 000000000000..35b0d4d2274c --- /dev/null +++ b/.maestro/flows/google_for_woo.yaml @@ -0,0 +1,83 @@ +# Smoke Test: Google for Woo +# P2 probe: hub.google-for-woo (not counted when the store is ineligible) +# P2 ref: Google for Woo > Trigger campaign creation, check webview loads +# +# Google for Woo is a CONDITIONAL hub-menu item — only shown when BOTH: +# 1. The site has the "Google Listings & Ads" plugin active at +# version >= 2.7.7 (see IsGoogleForWooEnabled.kt), AND +# 2. A Google Ads account is connected to that site. +# +# On stores where either is missing, the menu button never renders, so +# the flow must tolerate "Google for WooCommerce" being absent without +# failing the smoke run. When it IS present, we verify tapping it +# launches the Google Ads webview (GoogleAdsWebViewFragment, hosts a +# WebView with AppBarStatus.Hidden and pulls the Google campaign +# creation/dashboard URL). +# +# "Inbox" is the last item in the hub menu's general section +# (MoreMenuViewModel.generateGeneralSection) and always renders, so we +# scroll to it first to guarantee the whole menu has been traversed — +# after that, any conditional item above it that was going to render +# must already be in the view hierarchy. +appId: com.woocommerce.android.dev +name: "Google for Woo - Campaign creation webview" +tags: + - smoke_extended + - flaky_quarantine + - hub_menu +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/navigate_to_more_menu.yaml + +# ── Scroll through the menu so conditional items have a chance ─────── +# Inbox is always at the bottom of the general section. +- scrollUntilVisible: + element: "Inbox" + direction: DOWN + timeout: 15000 + label: "Scroll to bottom of hub menu" + +# ── Conditionally test Google for Woo ──────────────────────────────── +# If the plugin/ads-account gate passed, the row is visible somewhere +# in the menu. Scroll back up toward it and tap. +- runFlow: + when: + visible: "Google for WooCommerce" + commands: + - scrollUntilVisible: + element: "Google for WooCommerce" + direction: UP + timeout: 10000 + label: "Scroll to Google for WooCommerce" + - tapOn: + text: "Google for WooCommerce" + retryTapIfNoChange: true + label: "Tap Google for Woo menu item" + # Confirm the webview took over: the More Menu compose view is + # gone AND the Payments sibling menu item is not visible + # either (double-check we didn't just collapse). + - extendedWaitUntil: + notVisible: + id: "more_menu_compose_view" + timeout: 25000 + label: "Verify Google Ads webview took over" + - assertNotVisible: "Payments" + - takeScreenshot: google_for_woo + - back + - extendedWaitUntil: + visible: + id: "more_menu_compose_view" + timeout: 15000 + label: "Back on More Menu after Google Ads webview" + +# ── Skip-case screenshot ───────────────────────────────────────────── +# If Google for Woo wasn't available on this store, capture the menu +# state so the test report documents why the flow was a no-op. +- runFlow: + when: + notVisible: "Google for WooCommerce" + commands: + - takeScreenshot: google_for_woo_unavailable diff --git a/.maestro/flows/hub_menu_admin_and_store.yaml b/.maestro/flows/hub_menu_admin_and_store.yaml new file mode 100644 index 000000000000..fbbd4dbe354c --- /dev/null +++ b/.maestro/flows/hub_menu_admin_and_store.yaml @@ -0,0 +1,256 @@ +# Smoke Test: Hub Menu - WC Admin, View Store, Change store +# p2: hub.change-store, hub.wc-admin, hub.view-store +# P2 ref: Hub menu > WC Admin, View Store, Change store +# +# Verifies: +# - Tapping "View Store" launches the authenticated webview for the +# storefront (More Menu no longer visible) +# - Tapping "WC Admin" launches the authenticated webview for the +# WC Admin dashboard (More Menu no longer visible) +# - Dynamically discovers another visible Woo store, switches to it, proves +# its identity and Orders surface loaded, then switches back and repeats +# the same assertions for the original store +# +# Why we don't assert specific webview content: Maestro's text-match +# doesn't read into the embedded WebView reliably, and the rendered +# page depends on the staging site's current state. Asserting "we +# left the More Menu" + toolbar title is the highest-signal smoke +# check available. +appId: com.woocommerce.android.dev +name: "Hub Menu - WC Admin, View Store, Change store" +tags: + - smoke_extended + - flaky_quarantine + - hub_menu +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/navigate_to_more_menu.yaml + +# ── View Store ─────────────────────────────────────────────────────── +- scrollUntilVisible: + element: "View Store" + direction: DOWN + timeout: 15000 + label: "Scroll to View Store" + +- tapOn: + text: "View Store" + retryTapIfNoChange: true + label: "Open View Store" + +# AuthenticatedWebViewScreen uses the selected site's display name as +# the toolbar title. We don't know the display name, so instead assert +# the More Menu is no longer visible (its Compose view id should have +# been replaced by the webview fragment). +- extendedWaitUntil: + notVisible: + id: "more_menu_compose_view" + timeout: 20000 + label: "Verify webview took over from More Menu" + +# Give the webview a few seconds to render something before we +# screenshot so the capture shows real content, not a white flash. +- extendedWaitUntil: + visible: ".*" + timeout: 5000 + +- takeScreenshot: hub_view_store + +- back + +- extendedWaitUntil: + visible: + id: "more_menu_compose_view" + timeout: 15000 + label: "Back on More Menu" + +# ── WC Admin ───────────────────────────────────────────────────────── +- scrollUntilVisible: + element: "WC Admin" + direction: DOWN + timeout: 15000 + label: "Scroll to WC Admin" + +- tapOn: + text: "WC Admin" + retryTapIfNoChange: true + label: "Open WC Admin" + +# AuthenticatedWebViewScreen for WC Admin uses R.string.more_menu_ +# button_wс_admin = "WC Admin" as the toolbar title. That's also the +# tappable row we just left, so it isn't discriminative. Instead, +# confirm the More Menu is gone AND Payments (a sibling menu item) is +# no longer visible — proof we navigated off the More Menu. +- extendedWaitUntil: + notVisible: + id: "more_menu_compose_view" + timeout: 20000 + label: "Verify webview took over from More Menu" + +- assertNotVisible: "Payments" + +- takeScreenshot: hub_wc_admin + +- back + +- extendedWaitUntil: + visible: + id: "more_menu_compose_view" + timeout: 15000 + label: "Back on More Menu" + +# ── Change store ───────────────────────────────────────────────────── +# Require the visible title for diagnostics and use normalized URL-host +# equality for identity. Display names are not unique and are never used to +# decide whether the selected store changed. +- assertVisible: + id: "more_menu_store_title" +- copyTextFrom: + id: "more_menu_store_url" +- evalScript: ${output.originalStoreUrl = maestro.copiedText.trim()} +- assertTrue: + condition: ${output.originalStoreUrl.length > 0} + label: "Capture original store identity" + +- tapOn: + id: "more_menu_store_switcher" + label: "Open store picker" + +- extendedWaitUntil: + visible: ".*Select store.*|.*Connected Stores.*" + timeout: 15000 + label: "Require store picker" +- extendedWaitUntil: + visible: + id: "sites_recycler" + timeout: 20000 + label: "Wait for connected stores" + +# Woo stores are listed before non-Woo sites. Requiring two selectable domain +# rows makes the multi-store fixture contract explicit instead of silently +# passing after merely opening the picker. +- extendedWaitUntil: + visible: + id: "text_site_domain" + index: 1 + timeout: 20000 + label: "Require at least two visible connected stores" + +- copyTextFrom: + id: "text_site_domain" + index: 0 +- evalScript: ${output.storeCandidateZeroUrl = maestro.copiedText.trim()} +- copyTextFrom: + id: "text_site_domain" + index: 1 +- evalScript: ${output.storeCandidateOneUrl = maestro.copiedText.trim()} + +# Keep raw copied strings in output; Maestro 2.2.0 does not reliably export a +# regex-derived string across commands. Normalize to host only inside boolean +# conditions, where the result does not need to survive another command. +- runFlow: + when: + true: ${String(output.storeCandidateZeroUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0] != String(output.originalStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0]} + commands: + - evalScript: ${output.alternateStoreUrl = output.storeCandidateZeroUrl} + +- runFlow: + when: + true: ${String(output.storeCandidateZeroUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0] == String(output.originalStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0]} + commands: + - evalScript: ${output.alternateStoreUrl = output.storeCandidateOneUrl} + +- assertTrue: + condition: ${output.alternateStoreUrl && String(output.alternateStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0] != String(output.originalStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0]} + label: "Discover a different connected store" + +- tapOn: + text: "${output.alternateStoreUrl}" + label: "Select alternate store" +- tapOn: + id: "button_primary" + retryTapIfNoChange: true + label: "Confirm alternate store" + +- extendedWaitUntil: + visible: + id: "dashboard_container" + timeout: 30000 + label: "Wait for alternate store dashboard" + +- runFlow: + file: ../subflows/navigate_to_more_menu.yaml + +- copyTextFrom: + id: "more_menu_store_url" +- evalScript: ${output.selectedAlternateStoreUrl = maestro.copiedText.trim()} +- assertTrue: + condition: ${String(output.selectedAlternateStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0] == String(output.alternateStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0] && String(output.selectedAlternateStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0] != String(output.originalStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0]} + label: "Verify selected store identity changed" + +# Products is a store-backed surface. A real product or its legitimate empty +# state proves the alternate store loaded; network error states are rejected. +- runFlow: + file: ../subflows/navigate_to_products.yaml +- extendedWaitUntil: + visible: + id: "productName|empty_view" + timeout: 30000 + label: "Require alternate store Products result" +- assertNotVisible: ".*A network error occurred.*" +- assertNotVisible: ".*Your network is unavailable.*" + +# Restore the original store using the identity captured before the switch. +- runFlow: + file: ../subflows/navigate_to_more_menu.yaml +- tapOn: + id: "more_menu_store_switcher" + label: "Open store picker to restore original store" +- extendedWaitUntil: + visible: + id: "sites_recycler" + timeout: 20000 + +- scrollUntilVisible: + element: + text: ".*${String(output.originalStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0]}.*" + direction: DOWN + timeout: 15000 + label: "Find original store" +- tapOn: + text: ".*${String(output.originalStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0]}.*" + label: "Reselect original store" +- tapOn: + id: "button_primary" + retryTapIfNoChange: true + label: "Confirm original store" + +- extendedWaitUntil: + visible: + id: "dashboard_container" + timeout: 30000 + label: "Wait for restored store dashboard" + +- runFlow: + file: ../subflows/navigate_to_more_menu.yaml +- copyTextFrom: + id: "more_menu_store_url" +- evalScript: ${output.restoredStoreUrl = maestro.copiedText.trim()} +- assertTrue: + condition: ${String(output.restoredStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0] == String(output.originalStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0]} + label: "Verify original store identity restored" + +- runFlow: + file: ../subflows/navigate_to_products.yaml +- extendedWaitUntil: + visible: + id: "productName|empty_view" + timeout: 30000 + label: "Require restored store Products result" +- assertNotVisible: ".*A network error occurred.*" +- assertNotVisible: ".*Your network is unavailable.*" + +- takeScreenshot: hub_change_store_restored diff --git a/.maestro/flows/hub_menu_coupons.yaml b/.maestro/flows/hub_menu_coupons.yaml new file mode 100644 index 000000000000..2649bf842f75 --- /dev/null +++ b/.maestro/flows/hub_menu_coupons.yaml @@ -0,0 +1,126 @@ +# Smoke Test: Hub Menu - Coupons +# p2: hub.coupons.create +# P2 ref: Hub menu > Coupons > Create a coupon +# +# Verifies: +# - Coupons section reachable from the More Menu +# - Coupon list screen opens (either the populated list or the empty +# state — both include the add-coupon FAB) +# - Tap the add-coupon FAB to reach the "Create Coupon" type picker +# bottom sheet (Percentage, Fixed Cart, Fixed Product) +# - Pick Percentage Discount and confirm the edit-coupon screen +# opens +# - Create a run-scoped coupon and verify it appears in the list +appId: com.woocommerce.android.dev +name: "Hub Menu - Coupons" +tags: + - smoke_extended + - flaky_quarantine + - hub_menu + - destructive +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/navigate_to_more_menu.yaml + +# Coupons lives in the General section of the More Menu — scroll to it. +- scrollUntilVisible: + element: "Coupons" + direction: DOWN + timeout: 15000 + label: "Scroll to Coupons in hub menu" + +- tapOn: + text: "Coupons" + label: "Open Coupons" + +# ── Coupon list screen ─────────────────────────────────────────────── +# The add-coupon FAB (id: add_coupon_button, contentDescription "Add +# coupon") is always present on the coupon list screen — both for +# populated lists AND for the empty state. Asserting it is visible +# proves we actually navigated off the More Menu. +- extendedWaitUntil: + visible: + id: "add_coupon_button" + timeout: 15000 + +- takeScreenshot: hub_coupons_list + +# ── Open the coupon type picker ────────────────────────────────────── +- tapOn: + id: "add_coupon_button" + retryTapIfNoChange: true + label: "Tap add-coupon FAB" + +# CouponTypePickerScreen is a bottom sheet titled "Create Coupon" with +# three choices: Percentage Discount, Fixed Cart Discount, Fixed +# Product Discount. Asserting all three proves the whole sheet +# rendered, not just the title row. +- extendedWaitUntil: + visible: "Create Coupon" + timeout: 15000 + +- assertVisible: "Percentage Discount" +- assertVisible: "Fixed Cart Discount" +- assertVisible: "Fixed Product Discount" + +- takeScreenshot: hub_coupons_type_picker + +# ── Pick Percentage Discount → edit-coupon screen ──────────────────── +- tapOn: + text: "Percentage Discount" + label: "Choose Percentage Discount" + +# EditCouponFragment. Uniquely titled — the toolbar shows "Create +# coupon" (lowercase c) and the body has an amount field and a +# "Generate coupon code" / "Coupon code" CTA. "Amount" is present on +# every coupon type editor. +- extendedWaitUntil: + visible: ".*Amount.*" + timeout: 15000 + +- takeScreenshot: hub_coupons_edit_form + +- tapOn: + id: "edit_coupon_amount_input" +- eraseText +- inputText: "10" +- hideKeyboard + +- tapOn: + id: "edit_coupon_code_input" +- eraseText +- inputText: SUITE-${SUITE_RUN_ID}-UI +- hideKeyboard + +- tapOn: + id: "edit_coupon_save_button" + retryTapIfNoChange: true + label: "Create run-scoped coupon" + +# ── Back on coupon list ────────────────────────────────────────────── +- extendedWaitUntil: + visible: + id: "add_coupon_button" + timeout: 30000 + +- extendedWaitUntil: + visible: ".*${SUITE_RUN_ID}.*" + timeout: 30000 + label: "Wait for created coupon" + +- assertVisible: + text: ".*${SUITE_RUN_ID}.*" + label: "Created coupon is visible in the list" + +- takeScreenshot: hub_coupons_created + +# Back to the More Menu +- back + +- extendedWaitUntil: + visible: + id: "more_menu_compose_view" + timeout: 10000 diff --git a/.maestro/flows/hub_menu_customers_inbox.yaml b/.maestro/flows/hub_menu_customers_inbox.yaml new file mode 100644 index 000000000000..1aa4efad42cf --- /dev/null +++ b/.maestro/flows/hub_menu_customers_inbox.yaml @@ -0,0 +1,85 @@ +# Smoke Test: Hub Menu - Customers and Inbox +# p2: hub.customers, hub.inbox +# P2 ref: Hub menu > Customers, Inbox +# +# Verifies: +# - Customers section opens from the More Menu, the Customers +# screen renders with the "Search for customers" search hint +# - Inbox section opens from the More Menu, the Inbox screen +# renders (empty-state message OR an inbox note timestamp) +appId: com.woocommerce.android.dev +name: "Hub Menu - Customers and Inbox" +tags: + - smoke_extended + - flaky_quarantine + - hub_menu +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/navigate_to_more_menu.yaml + +# ── Customers ──────────────────────────────────────────────────────── +- scrollUntilVisible: + element: "Customers" + direction: DOWN + timeout: 15000 + label: "Scroll to Customers in hub menu" + +- tapOn: + text: "Customers" + label: "Open Customers" + +# The More Menu button AND the Customers screen top-bar both show +# "Customers", so the title alone doesn't prove navigation happened. +# Wait on the search hint ("Search for customers" — from +# order_creation_customer_search_hint), which only exists on the +# customer list screen. +- extendedWaitUntil: + visible: "Search for customers" + timeout: 15000 + +- assertVisible: "Customers" +- assertVisible: "Search for customers" + +- takeScreenshot: hub_customers + +# Go back to More Menu +- back + +- extendedWaitUntil: + visible: + id: "more_menu_compose_view" + timeout: 10000 + +# ── Inbox ──────────────────────────────────────────────────────────── +- scrollUntilVisible: + element: "Inbox" + direction: DOWN + timeout: 15000 + label: "Scroll to Inbox in hub menu" + +- tapOn: + text: "Inbox" + label: "Open Inbox" + +# Same problem as Customers: "Inbox" is also the More Menu button +# text. The Inbox screen renders either an empty state ("Congrats, +# you've read everything!" from empty_inbox_title) or a list of notes +# with "Moments ago" / "X hours ago" / "Created on ..." timestamps. +# Match on the empty state OR any of the recency labels. +- extendedWaitUntil: + visible: ".*Congrats.*|.*Moments ago.*|.*hours ago.*|.*days ago.*|.*minutes ago.*|.*Created on.*" + timeout: 20000 + label: "Wait for inbox content or empty state" + +- takeScreenshot: hub_inbox + +# Back to More Menu +- back + +- extendedWaitUntil: + visible: + id: "more_menu_compose_view" + timeout: 10000 diff --git a/.maestro/flows/hub_menu_payments.yaml b/.maestro/flows/hub_menu_payments.yaml new file mode 100644 index 000000000000..3a23fb19f32c --- /dev/null +++ b/.maestro/flows/hub_menu_payments.yaml @@ -0,0 +1,123 @@ +# Smoke Test: Hub Menu - Payments +# p2: hub.payments +# P2 ref: Hub menu > Payments (Enable/Disable Pay in Person, Order Card +# Reader, Manage Card Reader, card-reader help/docs, etc.) +# +# Verifies: +# - Payments screen opens from the More Menu +# - Payments hub renders every first-level item the P2 mentions: +# SETTINGS header, "Pay In Person" toggle, CARD READERS header, +# "Order Card Reader", "Manage Card Reader", card-reader help link +# - Tapping "Order Card Reader" opens its dedicated screen (P2-level +# navigation check) +# +# Why we don't actually toggle Pay-in-Person or tap Order Card Reader +# all the way through: Pay-in-Person toggling commits a real store +# setting change (noise on staging), and Order-a-card-reader exits to +# Stripe's webview which isn't a Maestro-friendly check. Presence of +# the items is the smoke-level guarantee P2 cares about. +appId: com.woocommerce.android.dev +name: "Hub Menu - Payments" +tags: + - smoke_extended + - flaky_quarantine + - hub_menu +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/navigate_to_more_menu.yaml + +# Payments sits high enough in the general section to usually be +# immediately visible, but scroll defensively in case the More Menu +# adds items above it. +- scrollUntilVisible: + element: "Payments" + direction: DOWN + timeout: 15000 + label: "Scroll to Payments in hub menu" + +- tapOn: + text: "Payments" + label: "Open Payments" + +# ── Payments hub loaded ────────────────────────────────────────────── +# "Pay In Person" (card_reader_enable_pay_in_person) is a toggleable +# row that ONLY appears in the Payments hub, not elsewhere, so it's a +# reliable anchor for "we landed on the hub". +- extendedWaitUntil: + visible: "Pay In Person" + timeout: 20000 + +- assertVisible: "Pay In Person" + +- takeScreenshot: hub_payments_top + +# ── Card Readers section ───────────────────────────────────────────── +# The SETTINGS section header renders first, then CARD READERS. Both +# are uppercase category titles, so matching is case-sensitive. If the +# store country doesn't support card readers, the CARD READERS header +# + sub-items won't render (addCardReaderManuals is guarded by +# CardReaderConfigForSupportedCountry). Our staging store is US-based +# so this is populated. +- scrollUntilVisible: + element: "Order Card Reader" + direction: DOWN + timeout: 10000 + label: "Scroll to Order Card Reader" + +- assertVisible: "Order Card Reader" +- assertVisible: "Manage Card Reader" + +- takeScreenshot: hub_payments_card_readers + +- scrollUntilVisible: + element: ".*accepting mobile payments.*ordering card readers.*" + direction: DOWN + timeout: 10000 + label: "Scroll to card-reader help link" + +- assertVisible: ".*accepting mobile payments.*ordering card readers.*" + +# ── Navigate into Order Card Reader ────────────────────────────────── +- extendedWaitUntil: + visible: "Pay In Person" + timeout: 15000 + +- scrollUntilVisible: + element: "Order Card Reader" + direction: DOWN + timeout: 10000 + label: "Re-scroll to Order Card Reader" + +- tapOn: + text: "Order Card Reader" + retryTapIfNoChange: true + label: "Open Order Card Reader" + +# Order-a-reader opens an authenticated webview loading the Stripe +# reader purchase page. Webview content isn't deterministic to assert +# on; just verify the back-button title (webview toolbar) or that we +# navigated away from the Payments hub (Pay In Person no longer +# visible). +- extendedWaitUntil: + notVisible: "Pay In Person" + timeout: 20000 + label: "Verify we left the Payments hub" + +- takeScreenshot: hub_payments_order_reader + +- back + +# ── Back to More Menu ──────────────────────────────────────────────── +- extendedWaitUntil: + visible: "Pay In Person" + timeout: 15000 + +- back + +- extendedWaitUntil: + visible: + id: "more_menu_compose_view" + timeout: 10000 diff --git a/.maestro/flows/hub_menu_settings.yaml b/.maestro/flows/hub_menu_settings.yaml new file mode 100644 index 000000000000..22f3e21e9120 --- /dev/null +++ b/.maestro/flows/hub_menu_settings.yaml @@ -0,0 +1,103 @@ +# Smoke Test: Hub Menu - Settings +# p2: hub.settings +# P2 ref: Hub menu > Settings > Smoke test all items (ignore Delete Account) +# +# Verifies: +# - More Menu top-level items are visible (Settings section + Payments) +# - Settings screen opens from the More Menu +# - Every first-level Settings item is rendered (P2: smoke-test all items) +# - Log Out button is visible at the bottom of the screen +appId: com.woocommerce.android.dev +name: "Hub Menu - Settings" +tags: + - smoke_extended + - flaky_quarantine + - hub_menu +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/navigate_to_more_menu.yaml + +# ── More Menu overview ─────────────────────────────────────────────── +# Two More Menu sections are visible from the top: Settings (with the +# Settings item, and — on WPCom non-free-trial stores only — +# Subscriptions) and the general section (Payments, WC Admin, View +# Store, Coupons, Reviews, Customers, Inbox). Assert the always-on +# anchors so a regressed menu layout is caught here. Subscriptions is +# hidden when isWpComStore is false (our staging store is +# Jetpack-connected / self-hosted), so it is not asserted. +- assertVisible: "Settings" +- assertVisible: "Payments" + +- takeScreenshot: hub_menu_overview + +# ── Open Settings ──────────────────────────────────────────────────── +# "Settings" appears twice: as the section title ("Settings" header) +# and as the row ("Settings / Update your preferences"). index: 0 is +# the section header, index: 1 is the tappable row. +- tapOn: + text: "Settings" + index: 1 + label: "Open Settings screen" + +# ── Smoke-test all first-level Settings items ──────────────────────── +# P2: "Smoke test all items (ignore Delete Account)". We assert each +# item renders — no need to open each sub-screen for a smoke check. +# Items are ordered top-to-bottom in fragment_settings_main.xml. +- extendedWaitUntil: + visible: "Help & support" + timeout: 15000 + +- assertVisible: "Help & support" +- assertVisible: "Troubleshoot Connection" + +- takeScreenshot: settings_screen_top + +# Scroll to reach items lower on the screen. Settings is a plain +# ScrollView, so scroll commands move the whole list. "Themes" +# (option_site_themes) is only visible on WPComAtomic sites, so we +# skip it — our staging Jetpack store doesn't render it. We scroll +# straight to Privacy settings, which is always present. +- scrollUntilVisible: + element: "Privacy settings" + direction: DOWN + timeout: 10000 + label: "Scroll to Privacy settings" + +- assertVisible: "Privacy settings" + +- scrollUntilVisible: + element: "Experimental features" + direction: DOWN + timeout: 10000 + label: "Scroll to Experimental features" + +- assertVisible: "Experimental features" + +- scrollUntilVisible: + element: "About the app" + direction: DOWN + timeout: 10000 + label: "Scroll to About section" + +- assertVisible: "About the app" + +- scrollUntilVisible: + element: "Log out" + direction: DOWN + timeout: 10000 + label: "Scroll to Log out" + +- assertVisible: "Log out" + +- takeScreenshot: settings_screen_bottom + +# Go back to More Menu +- back + +- extendedWaitUntil: + visible: + id: "more_menu_compose_view" + timeout: 10000 diff --git a/.maestro/flows/login_google.yaml b/.maestro/flows/login_google.yaml new file mode 100644 index 000000000000..72a50a260e6b --- /dev/null +++ b/.maestro/flows/login_google.yaml @@ -0,0 +1,141 @@ +# Smoke Test: Login - Social login (Google) +# p2: login.social-google +# P2 ref: Login > Social login - Apple/Google +# +# Verifies the "Continue with Google" path on the email screen launches +# the native Google account picker and that selecting an account reaches +# the dashboard. +# +# Preconditions (device/emulator): +# - A Google account is already signed in on the device. +# - The installed APK was built with the private WooCommerce +# google-services.json, not google-services.json-example. The example +# OAuth client lets the picker open but Google returns RESULT_CANCELED +# after account selection. The smoke runner validates this before launch. +# - That Google identity is LINKED on WP.com to an account with access +# to ${WOO_JETPACK_STORE_URL}. This is a stronger requirement than "a WP.com +# account with the same email exists": WP.com sign-in via Google +# uses the Google identity, not the email, so an account created +# via email+password will silently fail Google login unless the +# owner has explicitly linked Google under WP.com account settings. +# Symptom of a missing link: app shows "Logging in" briefly, then +# returns to the empty email screen with no visible error. +# +# Required env vars: +# WOO_JETPACK_STORE_URL (any Jetpack-connected store the linked account accesses) +appId: com.woocommerce.android.dev +name: "Login - Social login (Google)" +tags: + - smoke_extended + - flaky_quarantine + - login +--- +- launchApp: + clearState: true + +- extendedWaitUntil: + visible: ".*Skip.*|.*Enter your store address.*|.*Log in.*" + timeout: 30000 + +- runFlow: + when: + visible: "Skip" + commands: + - tapOn: "Skip" + +# Enter store URL to reach the email screen (Google button lives there) +- extendedWaitUntil: + visible: + id: "button_login_store" + timeout: 15000 +- tapOn: + id: "button_login_store" + retryTapIfNoChange: true + +- extendedWaitUntil: + visible: + id: "input" + timeout: 10000 +- tapOn: + id: "input" +- inputText: ${WOO_JETPACK_STORE_URL} +- hideKeyboard +- tapOn: + id: "bottom_button" + retryTapIfNoChange: true + +# Tap "Continue with Google" on the email screen +# (id: continue_with_google, text: "Continue with Google") +- extendedWaitUntil: + visible: + id: "continue_with_google" + timeout: 15000 +- tapOn: + id: "continue_with_google" + retryTapIfNoChange: true + +# Native Google account picker (Google Play Services bottom sheet). +# Content varies by device/Android version. We wait for any recognizable +# picker text, then tap the first email-looking entry. +- extendedWaitUntil: + visible: ".*Choose an account.*|.*Sign in with Google.*|.*@.*\\..*" + timeout: 20000 + +- takeScreenshot: login_google_account_picker + +- tapOn: + text: ".*@.*\\..*" + index: 0 + retryTapIfNoChange: true + +# Play Services may show a confirmation step after picker selection. +# The consent UI is a separate activity OUTSIDE the app, so gate each +# handler on confirming we're still on that activity — otherwise a plain +# `tapOn: "Continue"` / `"OK"` can fire on the app itself (e.g. hitting +# the email screen's "Continue with Google" button if we bounced back). +- runFlow: + when: + visible: ".*Continue as.*" + commands: + - tapOn: ".*Continue as.*" + +- runFlow: + when: + visible: ".*Sign in to.*|.*wants to access.*|.*Grant permission.*" + commands: + - tapOn: + text: "Allow|Continue" + +# If the WP.com account has access to multiple stores, the app shows +# a store-picker before the dashboard. Pick whichever one is first — +# we only need to reach the dashboard, not a specific store. +- runFlow: + when: + visible: ".*Choose your store.*|.*Select a store.*|.*Pick a store.*" + commands: + - tapOn: + id: "site_list_item" + index: 0 + - runFlow: + when: + visible: "Continue" + commands: + - tapOn: "Continue" + +# Verify dashboard loaded (allow generous time for the full auth +# round-trip: Play Services → Google → WP.com → site fetch → dashboard). +- extendedWaitUntil: + visible: ".*My store.*|.*Manage privacy.*|.*Dashboard.*" + timeout: 60000 + +- runFlow: + when: + visible: "Save" + commands: + - tapOn: "Save" + +- extendedWaitUntil: + visible: ".*My store.*|.*Dashboard.*" + timeout: 15000 + +- takeScreenshot: login_google_success diff --git a/.maestro/flows/login_help.yaml b/.maestro/flows/login_help.yaml new file mode 100644 index 000000000000..3b5120d79a65 --- /dev/null +++ b/.maestro/flows/login_help.yaml @@ -0,0 +1,63 @@ +# Smoke Test: Login - Help section +# p2: login.help +# P2 ref: Login > Help section +# +# Verifies the Help action in the login toolbar opens the help screen. +# The `?` help icon lives in the login toolbar (menu id: help, title +# "Help" from libs/login/res/menu/menu_login.xml), which is only +# present once the user has tapped through the prologue and reached +# the site-address screen — the prologue itself has no toolbar. +# +# No credentials required. +appId: com.woocommerce.android.dev +name: "Login - Help section" +tags: + - smoke_extended + - flaky_quarantine + - login +--- +- launchApp: + clearState: true + +- extendedWaitUntil: + visible: ".*Skip.*|.*Enter your store address.*|.*Log in.*" + timeout: 30000 + +- runFlow: + when: + visible: "Skip" + commands: + - tapOn: "Skip" + +# Enter the login flow to reach a screen with the toolbar (the prologue +# has no toolbar, so the help icon is only reachable from here on). +- extendedWaitUntil: + visible: + id: "button_login_store" + timeout: 15000 +- tapOn: + id: "button_login_store" + retryTapIfNoChange: true + +# Wait for the site-address screen's toolbar to settle +- extendedWaitUntil: + visible: + id: "input" + timeout: 10000 + +# Tap the help menu item. The Android menu sets id="@+id/help" and +# title="Help", so Maestro can locate it by either — prefer the id +# since the icon has no visible label. +- tapOn: + id: "help" + retryTapIfNoChange: true + +# Verify the help screen opened. The Zendesk-backed help module shows +# "Help Center" / "Contact support" / FAQ-style text. +- extendedWaitUntil: + visible: ".*Help Center.*|.*Contact support.*|.*FAQ.*|.*frequently asked.*|.*Zendesk.*" + timeout: 15000 + +- takeScreenshot: login_help_section + +- back diff --git a/.maestro/flows/login_no_jetpack.yaml b/.maestro/flows/login_no_jetpack.yaml new file mode 100644 index 000000000000..6ff72e5501b1 --- /dev/null +++ b/.maestro/flows/login_no_jetpack.yaml @@ -0,0 +1,115 @@ +# Smoke Test: Login - No Jetpack +# p2: login.no-jetpack +# P2 ref: Login > No Jetpack +# +# Logs into a WooCommerce site that does NOT have Jetpack connected +# using site credentials (not WP.com), and verifies the site-credentials +# path reaches the dashboard. +# +# Required env vars: +# WOO_NO_JETPACK_SITE_URL (any non-Jetpack WP site with WC) +# WOO_NO_JETPACK_SITE_ADMIN_USERNAME (site admin credentials — not WP.com) +# WOO_NO_JETPACK_SITE_ADMIN_PASSWORD +appId: com.woocommerce.android.dev +name: "Login - No Jetpack via site credentials" +tags: + - smoke_extended + - flaky_quarantine + - login +--- +- launchApp: + clearState: true + +- extendedWaitUntil: + visible: ".*Skip.*|.*Enter your store address.*|.*Log in.*" + timeout: 30000 + +- runFlow: + when: + visible: "Skip" + commands: + - tapOn: "Skip" + +# Enter no-Jetpack store URL +- extendedWaitUntil: + visible: + id: "button_login_store" + timeout: 15000 +- tapOn: + id: "button_login_store" + retryTapIfNoChange: true + +- extendedWaitUntil: + visible: + id: "input" + timeout: 10000 +- tapOn: + id: "input" +- inputText: ${WOO_NO_JETPACK_SITE_URL} +- hideKeyboard +- tapOn: + id: "bottom_button" + retryTapIfNoChange: true + +# For a site without Jetpack the app usually routes directly to the +# site-credentials login screen, skipping the email screen entirely. +# If it didn't (e.g. the site reports partial Jetpack support), the +# email screen will show a "Log in with your site credentials" button +# (id: login_site_creds) that we need to tap first. +- runFlow: + when: + visible: + id: "login_site_creds" + commands: + - tapOn: + id: "login_site_creds" + retryTapIfNoChange: true + +# Site-credentials screen shows Username + Password on the SAME screen. +# This screen is rendered by the libs/login Compose tree, which does +# NOT apply `testTagsAsResourceId = true` (that's only on the app's +# WooTheme), so UI nodes expose NO resource IDs to Maestro. Drive it +# with visible text selectors ("Username", "Password", "Continue"), +# which are always present as Material TextInputLayout labels. +- extendedWaitUntil: + visible: "Username" + timeout: 15000 +- tapOn: "Username" +- inputText: ${WOO_NO_JETPACK_SITE_ADMIN_USERNAME} + +# Tap the password label directly — its Material TextInputLayout focuses +# the associated EditText. Avoid hideKeyboard here: the site-creds screen +# uses a custom input rail that doesn't expose a standard dismiss action +# (Maestro errors with "Couldn't hide the keyboard"), and Continue is +# visible above the IME anyway. +- tapOn: "Password" +- inputText: ${WOO_NO_JETPACK_SITE_ADMIN_PASSWORD} + +- tapOn: "Continue" + +# After a successful site-credentials login, the app lands on the +# dashboard and shows the "Manage privacy" bottom sheet first. Dismiss +# it (Save accepts the defaults) so the dashboard cards are tappable +# and visible for the Jetpack-banner assertion below. +- extendedWaitUntil: + visible: ".*Manage privacy.*|.*My store.*" + timeout: 60000 +- runFlow: + when: + visible: ".*Manage privacy.*" + commands: + - tapOn: "Save" + +# Verify the site-credentials path completed and landed on the store +# dashboard. The dashboard content for a fresh non-Jetpack site changes +# with remote feature flags and store setup state, so assert the stable +# dashboard container instead of a specific promotional card. +- extendedWaitUntil: + visible: "My store" + timeout: 10000 + +- assertVisible: + id: "dashboard_container" + label: "Verify dashboard loaded after no-Jetpack site login" + +- takeScreenshot: login_no_jetpack diff --git a/.maestro/flows/login_not_woo_store.yaml b/.maestro/flows/login_not_woo_store.yaml new file mode 100644 index 000000000000..72a1041685c2 --- /dev/null +++ b/.maestro/flows/login_not_woo_store.yaml @@ -0,0 +1,136 @@ +# Smoke Test: Login - Not a WooCommerce store +# p2: login.not-woo-store +# P2 ref: Login > Not a Woo store (notawoostore.wordpress.com) +# +# Logs into a WordPress site that does NOT have WooCommerce installed. +# Site-admin credentials are preferred, with WP.com credentials used when +# the site routes through WordPress.com authentication instead. +# +# Required env vars: +# WOO_NOT_A_WOO_STORE_URL +# WOO_NOT_A_WOO_STORE_SITE_ADMIN_USERNAME +# WOO_NOT_A_WOO_STORE_SITE_ADMIN_PASSWORD +# Optional WP.com fallback env vars (set both or neither): +# WOO_NOT_A_WOO_STORE_WPCOM_EMAIL +# WOO_NOT_A_WOO_STORE_WPCOM_PASSWORD +appId: com.woocommerce.android.dev +name: "Login - Not a WooCommerce store error" +tags: + - smoke_extended + - flaky_quarantine + - login +--- +- launchApp: + clearState: true + +- extendedWaitUntil: + visible: ".*Skip.*|.*Enter your store address.*|.*Log in.*" + timeout: 30000 + +- runFlow: + when: + visible: "Skip" + commands: + - tapOn: "Skip" + +# Enter store address +- extendedWaitUntil: + visible: + id: "button_login_store" + timeout: 15000 +- tapOn: + id: "button_login_store" + retryTapIfNoChange: true + +- extendedWaitUntil: + visible: + id: "input" + timeout: 10000 +- tapOn: + id: "input" +- inputText: ${WOO_NOT_A_WOO_STORE_URL} +- hideKeyboard +- tapOn: + id: "bottom_button" + retryTapIfNoChange: true + +# Wait for either authentication route. A site can route directly to the +# site-credentials form or expose that path from the WP.com email screen. +- extendedWaitUntil: + visible: "Username|Email address" + timeout: 30000 + +# Prefer site-admin credentials when the app offers that route explicitly. +- runFlow: + when: + visible: + id: "login_site_creds" + commands: + - tapOn: + id: "login_site_creds" + retryTapIfNoChange: true + - extendedWaitUntil: + visible: "Username" + timeout: 15000 + +# Self-hosted sites show Username and Password on the same screen. +- runFlow: + when: + visible: "Username" + commands: + - tapOn: "Username" + - inputText: ${WOO_NOT_A_WOO_STORE_SITE_ADMIN_USERNAME} + - tapOn: "Password" + - inputText: ${WOO_NOT_A_WOO_STORE_SITE_ADMIN_PASSWORD} + - tapOn: "Continue" + +# Fall back to WP.com credentials when no site-admin route is available. +- runFlow: + when: + visible: + id: "login_continue_button" + commands: + - tapOn: + id: "input" + - inputText: ${WOO_NOT_A_WOO_STORE_WPCOM_EMAIL} + - hideKeyboard + - tapOn: + id: "login_continue_button" + retryTapIfNoChange: true + - runFlow: + when: + visible: "No thanks" + commands: + - tapOn: "No thanks" + - runFlow: + when: + visible: + id: "login_enter_password" + commands: + - tapOn: + id: "login_enter_password" + - runFlow: + when: + visible: "No thanks" + commands: + - tapOn: "No thanks" + - extendedWaitUntil: + visible: + id: "input" + timeout: 10000 + - tapOn: + id: "input" + - inputText: ${WOO_NOT_A_WOO_STORE_WPCOM_PASSWORD} + - hideKeyboard + - tapOn: + id: "bottom_button" + retryTapIfNoChange: true + +# Verify the configured WordPress site is rejected specifically because it +# does not have WooCommerce. A Jetpack-account error does not prove this P2 +# check and must fail so the fixture can be repaired. +- extendedWaitUntil: + visible: ".*not a WooCommerce site.*" + timeout: 30000 + +- takeScreenshot: login_not_woo_store diff --git a/.maestro/flows/login_not_wp_site.yaml b/.maestro/flows/login_not_wp_site.yaml new file mode 100644 index 000000000000..9fae0ffd50ec --- /dev/null +++ b/.maestro/flows/login_not_wp_site.yaml @@ -0,0 +1,58 @@ +# Smoke Test: Login - Not a WordPress site +# p2: login.not-wp-site +# P2 ref: Login > Not a WP site (use "Google.com" or any non-WordPress site) +# +# Manages its own login flow (does not call the login subflow) because +# we're validating the pre-login error path. Starts from a fresh state. +appId: com.woocommerce.android.dev +name: "Login - Not a WordPress site error" +tags: + - smoke_extended + - flaky_quarantine + - login +--- +- launchApp: + clearState: true + +# Wait for app to finish loading after clearState +- extendedWaitUntil: + visible: ".*Skip.*|.*Enter your store address.*|.*Log in.*" + timeout: 30000 + +# Skip carousel if shown +- runFlow: + when: + visible: "Skip" + commands: + - tapOn: "Skip" + +# Tap "Enter your store address" +- extendedWaitUntil: + visible: + id: "button_login_store" + timeout: 15000 +- tapOn: + id: "button_login_store" + retryTapIfNoChange: true + +# Enter a non-WordPress site URL +- extendedWaitUntil: + visible: + id: "input" + timeout: 10000 +- tapOn: + id: "input" +- inputText: "google.com" +- tapOn: + id: "bottom_button" + retryTapIfNoChange: true + +# Verify error message appears +- extendedWaitUntil: + visible: ".*not able to detect a WordPress site.*" + timeout: 15000 + +# Verify the recovery buttons are present +- assertVisible: "Try another store" + +- takeScreenshot: login_not_wp_site diff --git a/.maestro/flows/login_successful.yaml b/.maestro/flows/login_successful.yaml new file mode 100644 index 000000000000..149c49e5d71a --- /dev/null +++ b/.maestro/flows/login_successful.yaml @@ -0,0 +1,33 @@ +# Smoke Test: Successful Login via Store Address +# p2: login.jetpack +# P2 ref: Login > Jetpack (site address + WP.com credentials) +# +# Prerequisites: +# - WOO_JETPACK_STORE_URL, WOO_WPCOM_EMAIL, WOO_WPCOM_PASSWORD env vars set +# +# Run: +# maestro test \ +# -e WOO_WPCOM_EMAIL="..." \ +# -e WOO_WPCOM_PASSWORD="..." \ +# -e WOO_JETPACK_STORE_URL="..." \ +# .maestro/flows/login_successful.yaml +appId: com.woocommerce.android.dev +name: "Login - Successful store login" +tags: + - smoke_core + - login +--- +# Login +- runFlow: + file: ../subflows/login.yaml + +# Verify we landed on the Dashboard +- assertVisible: + id: "dashboard" + label: "Verify Dashboard tab is visible after login" + +- assertVisible: + id: "dashboard_container" + label: "Verify Dashboard container loaded" + +- takeScreenshot: login_successful diff --git a/.maestro/flows/login_wrong_account.yaml b/.maestro/flows/login_wrong_account.yaml new file mode 100644 index 000000000000..4414e71a98b6 --- /dev/null +++ b/.maestro/flows/login_wrong_account.yaml @@ -0,0 +1,103 @@ +# Smoke Test: Login - Wrong account for the store +# p2: login.wrong-account +# P2 ref: Login > Wrong account for the store +# +# Logs into a WooCommerce store using a WP.com account that does NOT have +# access to it, and verifies the "account not connected to Jetpack" error. +# +# Required env vars: +# WOO_WPCOM_EMAIL, WOO_WPCOM_PASSWORD +# (selected WP.com account) +# WOO_WRONG_ACCOUNT_STORE_URL (site the primary account can't access) +appId: com.woocommerce.android.dev +name: "Login - Wrong account for the store" +tags: + - smoke_extended + - flaky_quarantine + - login +--- +- launchApp: + clearState: true + +- extendedWaitUntil: + visible: ".*Skip.*|.*Enter your store address.*|.*Log in.*" + timeout: 30000 + +- runFlow: + when: + visible: "Skip" + commands: + - tapOn: "Skip" + +- extendedWaitUntil: + visible: + id: "button_login_store" + timeout: 15000 +- tapOn: + id: "button_login_store" + retryTapIfNoChange: true + +- extendedWaitUntil: + visible: + id: "input" + timeout: 10000 +- tapOn: + id: "input" +- inputText: ${WOO_WRONG_ACCOUNT_STORE_URL} +- hideKeyboard +- tapOn: + id: "bottom_button" + retryTapIfNoChange: true + +- extendedWaitUntil: + visible: + id: "login_continue_button" + timeout: 15000 +- tapOn: + id: "input" +- inputText: ${WOO_WPCOM_EMAIL} +- hideKeyboard +- tapOn: + id: "login_continue_button" + retryTapIfNoChange: true + +- runFlow: + when: + visible: "No thanks" + commands: + - tapOn: "No thanks" + +- runFlow: + when: + visible: + id: "login_enter_password" + commands: + - tapOn: + id: "login_enter_password" + +- runFlow: + when: + visible: "No thanks" + commands: + - tapOn: "No thanks" + +- extendedWaitUntil: + visible: + id: "input" + timeout: 10000 +- tapOn: + id: "input" +- inputText: ${WOO_WPCOM_PASSWORD} +- hideKeyboard +- tapOn: + id: "bottom_button" + retryTapIfNoChange: true + +# Verify "account not connected" error +# String: login_jetpack_not_connected = +# "It looks like your account is not connected to %1$s's Jetpack" +- extendedWaitUntil: + visible: ".*not connected.*Jetpack.*|.*account.*not connected.*" + timeout: 30000 + +- takeScreenshot: login_wrong_account diff --git a/.maestro/flows/login_wrong_credentials.yaml b/.maestro/flows/login_wrong_credentials.yaml new file mode 100644 index 000000000000..7d3b32537ed0 --- /dev/null +++ b/.maestro/flows/login_wrong_credentials.yaml @@ -0,0 +1,106 @@ +# Smoke Test: Login - Wrong credentials +# p2: login.wrong-credentials +# P2 ref: Login > Wrong credentials +# +# Manages its own login flow (does not call the login subflow) because +# we're validating the wrong-password error path. Uses a real email +# so the account lookup succeeds and the password screen is reached. +# Credentials are available via MAESTRO_ prefix env vars automatically. +appId: com.woocommerce.android.dev +name: "Login - Wrong credentials error" +tags: + - smoke_extended + - flaky_quarantine + - login +--- +- launchApp: + clearState: true + +# Wait for app to finish loading after clearState +- extendedWaitUntil: + visible: ".*Skip.*|.*Enter your store address.*|.*Log in.*" + timeout: 30000 + +# Skip carousel if shown +- runFlow: + when: + visible: "Skip" + commands: + - tapOn: "Skip" + +# Enter store address +- extendedWaitUntil: + visible: + id: "button_login_store" + timeout: 15000 +- tapOn: + id: "button_login_store" + retryTapIfNoChange: true + +- extendedWaitUntil: + visible: + id: "input" + timeout: 10000 +- tapOn: + id: "input" +- inputText: ${WOO_JETPACK_STORE_URL} +- tapOn: + id: "bottom_button" + retryTapIfNoChange: true + +# Enter email +- extendedWaitUntil: + visible: + id: "input" + timeout: 15000 +- tapOn: + id: "input" +- inputText: ${WOO_WPCOM_EMAIL} +- tapOn: + id: "login_continue_button" + retryTapIfNoChange: true + +# Dismiss Google Password Manager if it appears +- runFlow: + when: + visible: "No thanks" + commands: + - tapOn: "No thanks" + +# Handle Magic Link screen +- runFlow: + when: + visible: + id: "login_enter_password" + commands: + - tapOn: + id: "login_enter_password" + +# Dismiss Google Password Manager again if it appears on password screen +- runFlow: + when: + visible: "No thanks" + commands: + - tapOn: "No thanks" + +# Enter wrong password +- extendedWaitUntil: + visible: + id: "input" + timeout: 10000 +- tapOn: + id: "input" +- inputText: "definitely_wrong_password_12345" +- tapOn: + id: "bottom_button" + retryTapIfNoChange: true + +# Verify error message appears. The Woo app shows one of: +# - "It looks like this password is incorrect..." (libs/login) +# - "It seems the username or password you entered doesn't quite match..." (WooCommerce) +# - "The username or password you entered is incorrect" (fallback) +- extendedWaitUntil: + visible: ".*password.*incorrect.*|.*doesn't quite match.*|.*username or password.*incorrect.*" + timeout: 15000 + +- takeScreenshot: login_wrong_credentials diff --git a/.maestro/flows/orders_barcode_scanner_opens.yaml b/.maestro/flows/orders_barcode_scanner_opens.yaml new file mode 100644 index 000000000000..cf1287c292c1 --- /dev/null +++ b/.maestro/flows/orders_barcode_scanner_opens.yaml @@ -0,0 +1,99 @@ +# Smoke Test: Orders - Barcode scanner opens +# p2: orders.barcode.open-scanner +# P2 ref: Orders > Barcode scanner entry points +# +# Verifies: +# - Orders-list barcode action opens the scanner surface +# - Empty order-creation "Add products via scanner" opens the scanner +# - The draft order screen is discarded without creating an order +# +# This flow does not claim barcode product injection or order creation. Those +# still require a camera feed or a guided physical barcode scan. +appId: com.woocommerce.android.dev +name: "Orders - Barcode scanner opens" +tags: + - smoke_extended + - flaky_quarantine + - orders + - system_surface +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/navigate_to_orders.yaml + +# Orders-list scanner entry. +- tapOn: + id: "menu_barcode" + retryTapIfNoChange: true + label: "Open scanner from orders list" + +- extendedWaitUntil: + visible: + id: "barcodeComposeView" + timeout: 15000 + label: "Wait for barcode scanner surface" + +- extendedWaitUntil: + visible: ".*Scan Product Barcode.*|.*Grant Camera Permission.*" + timeout: 15000 + label: "Wait for barcode scanner overlay" + +- takeScreenshot: orders_barcode_scanner_from_list + +- back + +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 15000 + +# Order-creation product scanner entry. +- tapOn: + id: "createOrderButton" + retryTapIfNoChange: true + label: "Tap Create Order FAB" + +- extendedWaitUntil: + visible: "Add products" + timeout: 15000 + +- tapOn: + text: ".*Add products via scanner.*|.*Scan products.*" + retryTapIfNoChange: true + label: "Open scanner from order creation" + +- extendedWaitUntil: + visible: + id: "barcodeComposeView" + timeout: 15000 + label: "Wait for barcode scanner surface from order creation" + +- extendedWaitUntil: + visible: ".*Scan Product Barcode.*|.*Grant Camera Permission.*" + timeout: 15000 + label: "Wait for order-creation scanner overlay" + +- takeScreenshot: orders_barcode_scanner_from_order_creation + +- back + +- extendedWaitUntil: + visible: "New order" + timeout: 15000 + +- back + +- runFlow: + when: + visible: "Discard" + commands: + - tapOn: + text: "Discard" + label: "Discard empty order draft" + +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 15000 diff --git a/.maestro/flows/orders_cash_payment.yaml b/.maestro/flows/orders_cash_payment.yaml new file mode 100644 index 000000000000..9861809f538f --- /dev/null +++ b/.maestro/flows/orders_cash_payment.yaml @@ -0,0 +1,158 @@ +# Smoke Test: Orders - Collect cash (Cash on Delivery) payment +# p2: orders.collect-payment.cash +# P2 ref: Orders > Collect payment using cash on delivery +# +# Verifies: +# - Create a fresh unpaid order through the app UI +# - Tap "Collect Payment" in the payment info section +# - Pick the "Cash" option on the Select Payment Method screen +# - Change Due Calculator opens with the order total pre-filled +# - Tap "Mark Order as Complete" to record the cash payment +# - Returns to the order detail/list with the order marked completed +# +# Note: the cash-payment flow has no undo — the order stays Completed +# with a "Cash on Delivery" gateway recorded. It therefore creates its +# own pending-payment order first instead of consuming an arbitrary row +# from the shared store. +appId: com.woocommerce.android.dev +name: "Orders - Collect cash payment" +tags: + - smoke_extended + - flaky_quarantine + - orders + - destructive +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/navigate_to_orders.yaml + +# ── Create a fresh pending-payment order ────────────────────────────── +- tapOn: + id: "createOrderButton" + retryTapIfNoChange: true + label: "Tap Create Order FAB" + +- extendedWaitUntil: + visible: "Add products" + timeout: 15000 + +- tapOn: + text: "Add products" + label: "Tap Add products" + +- extendedWaitUntil: + visible: ".*Search Products.*" + timeout: 15000 + +- tapOn: + point: "50%,30%" + label: "Tap first product row" + +- extendedWaitUntil: + visible: ".*Select \\d+ Product.*" + timeout: 15000 + +- tapOn: + text: ".*Select \\d+ Product.*" + label: "Confirm product selection" + +- extendedWaitUntil: + visible: "New order" + timeout: 15000 + +- takeScreenshot: cash_payment_order_ready_to_create + +# The toolbar Create action submits the order without starting the +# payment flow. That leaves a safe automation-owned Pending payment +# order whose detail screen exposes Collect Payment. +- tapOn: + id: "menu_create" + retryTapIfNoChange: true + label: "Create pending-payment order" + +- extendedWaitUntil: + visible: + id: "orderDetail_container" + timeout: 15000 + +- extendedWaitUntil: + visible: ".*Pending payment.*" + timeout: 20000 + label: "Wait for pending-payment order detail" + +- takeScreenshot: order_detail_before_collect + +# ── Collect Payment → Cash ─────────────────────────────────────────── +- scrollUntilVisible: + element: + id: "paymentInfo_collectCardPresentPaymentButton" + direction: DOWN + timeout: 15000 + label: "Scroll to Collect Payment button" + +- tapOn: + id: "paymentInfo_collectCardPresentPaymentButton" + retryTapIfNoChange: true + label: "Tap Collect Payment" + +# Occasional flake: on the first tap the button visibly depresses but +# the SelectPaymentMethod fragment never presents. Observed after the +# orders_mark_complete flow runs earlier in the suite — the app's back +# stack is transiently non-ideal. Give the normal path 10s, and if the +# sheet still hasn't appeared, tap again (the first tap was lost). On +# the happy path the second tap is a no-op because the button is no +# longer part of the current screen. +- runFlow: + when: + notVisible: ".*Take payment.*|.*Cash.*" + commands: + - extendedWaitUntil: + visible: ".*Take payment.*|.*Cash.*" + timeout: 10000 + +- runFlow: + when: + notVisible: ".*Take payment.*|.*Cash.*" + commands: + - tapOn: + id: "paymentInfo_collectCardPresentPaymentButton" + retryTapIfNoChange: true + label: "Retry Collect Payment tap (first was lost)" + +- extendedWaitUntil: + visible: ".*Take payment.*|.*Cash.*" + timeout: 30000 + +- takeScreenshot: select_payment_method + +- tapOn: + text: "Cash" + retryTapIfNoChange: true + label: "Select Cash payment method" + +# ── Change Due Calculator ──────────────────────────────────────────── +- extendedWaitUntil: + visible: ".*Mark Order as Complete.*" + timeout: 15000 + +- takeScreenshot: change_due_calculator + +- tapOn: + text: ".*Mark Order as Complete.*" + retryTapIfNoChange: true + label: "Mark order as complete with cash" + +# ── Verify order is marked completed and we returned to the list ───── +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 30000 + +- takeScreenshot: orders_after_cash_payment + +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 10000 diff --git a/.maestro/flows/orders_create.yaml b/.maestro/flows/orders_create.yaml new file mode 100644 index 000000000000..d6bccf2fbbaf --- /dev/null +++ b/.maestro/flows/orders_create.yaml @@ -0,0 +1,743 @@ +# Smoke Test: Orders - Create Order +# p2: orders.create.products, orders.create.variable-products, orders.create.quantity, orders.create.product-discount, orders.create.custom-amount, orders.create.custom-amount-note, orders.create.shipping, orders.create.customer-add, orders.create.customer-edit, orders.create.note +# P2 ref: Orders > Create an order (add products, custom amount, customer, customer note) +# +# Verifies: +# - Order creation FAB works +# - Add a product from the product selector +# - Add a fixed-amount custom amount with a name +# - Add an existing customer by searching their email +# - Prove the selected customer's identity is attached to the draft +# - Edit only the order-local customer copy and persist its marker +# - Prove the underlying store customer remains unchanged +# - Add a customer-facing note +# - All sections display the added data +# - Tap "Create" to submit the order — lands on order detail +# - Tap "Collect Payment" → pick Cash → Mark Order as Complete to +# fully exercise the post-creation cash checkout path +# +# The order IS committed to the staging store and completed via COD. +# Staging orders use test data (SmokeTest customer, $10 gift wrap) so +# accumulation across runs is acceptable. +appId: com.woocommerce.android.dev +name: "Orders - Create a new order" +tags: + - smoke_extended + - flaky_quarantine + - orders + - destructive +--- +# Start from an already-authenticated session (see comment in +# ensure_logged_in.yaml — re-logging in on every run triggers WPCom +# security screens). +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/ensure_configured_woo_store.yaml + +# Navigate to Orders +- runFlow: + file: ../subflows/navigate_to_orders.yaml + +# Tap Create Order FAB +- tapOn: + id: "createOrderButton" + retryTapIfNoChange: true + label: "Tap Create Order FAB" + +# Wait for order creation screen (title and Add products are always visible on empty creation) +- extendedWaitUntil: + visible: "Add products" + timeout: 15000 + +- takeScreenshot: order_create_empty + +# ── Add a product ───────────────────────────────────────────────────── +- tapOn: + text: "Add products" + label: "Tap Add products" + +# Product selector loaded +- extendedWaitUntil: + visible: ".*Search Products.*" + timeout: 15000 + +- takeScreenshot: order_create_product_selector + +# Filter to Variable products so this order always exercises the variation +# picker instead of depending on whichever product happens to be listed first. +- tapOn: + text: "Filter" + retryTapIfNoChange: true + label: "Open product selector filters" + +- extendedWaitUntil: + visible: + id: "filterList" + timeout: 15000 + label: "Wait for product selector filters" + +- tapOn: + text: "Product type" + retryTapIfNoChange: true + label: "Open Product type filter" + +- extendedWaitUntil: + visible: + id: "filterOptionList" + timeout: 15000 + label: "Wait for Product type options" + +- tapOn: + text: "Variable" + retryTapIfNoChange: true + label: "Select Variable products" + +- tapOn: + id: "filterOptionList_btnShowProducts" + retryTapIfNoChange: true + label: "Confirm Variable product filter" + +- runFlow: + when: + visible: + id: "filterList_btnShowProducts" + commands: + - tapOn: + id: "filterList_btnShowProducts" + retryTapIfNoChange: true + label: "Apply Variable product filter" + +- extendedWaitUntil: + visible: + id: "product_selector_variable_product_item" + timeout: 20000 + label: "Require a Variable product with variations" + +- tapOn: + id: "product_selector_variable_product_item" + index: 0 + retryTapIfNoChange: true + label: "Open the first Variable product" + +- extendedWaitUntil: + visible: + id: "product_selector_variation_item" + timeout: 15000 + label: "Require a purchasable variation" + +- tapOn: + id: "product_selector_variation_item" + index: 0 + label: "Select first available variation" + +- back + +- extendedWaitUntil: + visible: + id: "product_selector_done_button" + timeout: 15000 + label: "Wait for selected variation in parent selector" + +# Keep the selected variation, switch the selector to Simple products, and +# select another purchasable line item. Filtering by a different product type +# guarantees the second item cannot be the selected variation or its parent. +- tapOn: + text: "Filter.*" + retryTapIfNoChange: true + label: "Reopen product selector filters" + +- extendedWaitUntil: + visible: + id: "filterList" + timeout: 15000 + +- tapOn: + text: "Product type" + retryTapIfNoChange: true + label: "Change Product type filter" + +- extendedWaitUntil: + visible: + id: "filterOptionList" + timeout: 15000 + +- tapOn: + text: "Simple" + retryTapIfNoChange: true + label: "Select Simple products" + +- tapOn: + id: "filterOptionList_btnShowProducts" + retryTapIfNoChange: true + label: "Confirm Simple product filter" + +- runFlow: + when: + visible: + id: "filterList_btnShowProducts" + commands: + - tapOn: + id: "filterList_btnShowProducts" + retryTapIfNoChange: true + label: "Apply Simple product filter" + +- extendedWaitUntil: + visible: + id: "product_selector_product_item" + timeout: 20000 + label: "Require a purchasable Simple product" + +- tapOn: + id: "product_selector_product_item" + index: 0 + label: "Select a distinct Simple product" + +- tapOn: + id: "product_selector_done_button" + label: "Confirm both product selections" + +# Back on order creation +- extendedWaitUntil: + visible: "New order" + timeout: 15000 + +- assertVisible: + id: "order_product_card" + index: 0 + label: "First selected line item is present in the order" + +- assertVisible: + id: "order_product_card" + index: 1 + label: "Second selected line item is present in the order" + +- assertVisible: + text: ".*⦁.*" + label: "A selected line item exposes real variation attributes" + +- takeScreenshot: order_create_with_product + +# ── Adjust quantity and apply a product discount ───────────────────── +- tapOn: + id: "order_product_card" + index: 0 + retryTapIfNoChange: true + label: "Expand first selected product" + +- extendedWaitUntil: + visible: "Order count" + timeout: 10000 + +- tapOn: + text: "Increase product quantity" + retryTapIfNoChange: true + label: "Increase product quantity" + +- assertVisible: + text: ".*2 ×.*|.*2 x.*" + label: "Quantity increased to two" + +- tapOn: + text: "Decrease product quantity" + retryTapIfNoChange: true + label: "Decrease product quantity" + +- assertVisible: + text: ".*1 ×.*|.*1 x.*" + label: "Quantity decreased to one" + +- tapOn: + text: "Add discount" + retryTapIfNoChange: true + label: "Open product discount" + +- extendedWaitUntil: + visible: "Discount" + timeout: 15000 + +# Use a percentage so the fixture's price cannot make the discount invalid. +- tapOn: + text: "%" + label: "Use percentage discount" +- eraseText: 100 +- inputText: "10" +- hideKeyboard + +- assertVisible: + text: "Calculated Amount" + label: "Percentage discount is calculated" +- assertNotVisible: "Discount cannot be greater than the price" + +- tapOn: + text: "Done" + retryTapIfNoChange: true + label: "Save product discount" + +- extendedWaitUntil: + visible: "New order" + timeout: 15000 + +- assertVisible: + id: "order_product_discount_amount" + label: "Product discount is reflected in the order" +- copyTextFrom: + id: "order_product_discount_amount" +- evalScript: | + if (!maestro.copiedText || maestro.copiedText.trim().charAt(0) !== '-') { + throw new Error('Product discount amount is missing from the order'); + } + +# ── Add a custom amount (fixed, with a name) ────────────────────────── +- scrollUntilVisible: + element: "Add custom amount" + direction: DOWN + timeout: 10000 + label: "Scroll to custom amounts section" + +- tapOn: + text: "Add custom amount" + label: "Tap Add custom amount" + +# Same flake pattern as the orders_cash_payment Collect Payment tap: +# occasionally the first tap visibly depresses the row but the "Select +# custom amount type" bottom sheet never presents. Give the happy path +# 8s, then re-tap if the sheet still isn't there (the first tap was +# lost). On the normal path the re-tap is skipped because the sheet is +# already visible. +- runFlow: + when: + notVisible: "A fixed amount" + commands: + - extendedWaitUntil: + visible: "A fixed amount" + timeout: 8000 + +- runFlow: + when: + notVisible: "A fixed amount" + commands: + - tapOn: + text: "Add custom amount" + retryTapIfNoChange: true + label: "Retry tap on Add custom amount (first was lost)" + +- extendedWaitUntil: + visible: "A fixed amount" + timeout: 15000 + +- tapOn: + text: "A fixed amount" + label: "Choose fixed amount" + +# Fill amount and name in the custom amount dialog +- extendedWaitUntil: + visible: + id: "editPrice" + timeout: 10000 + +- tapOn: + id: "editPrice" +- eraseText +# Emulator locale uses comma as decimal separator — entering "1000" yields 10,00 +- inputText: "1000" +- hideKeyboard + +- tapOn: + id: "customAmountNameText" +- inputText: "Gift wrap" +- hideKeyboard + +- takeScreenshot: order_create_custom_amount_dialog + +- tapOn: + id: "buttonDone" + label: "Tap Done on custom amount" + +# Back on order creation — verify custom amount was added +- extendedWaitUntil: + visible: "New order" + timeout: 15000 + +- assertVisible: + text: "Gift wrap" + label: "Custom amount name is visible in creation screen" + +- takeScreenshot: order_create_with_custom_amount + +# ── Add shipping ───────────────────────────────────────────────────── +- scrollUntilVisible: + element: + id: "add_shipping_button" + direction: DOWN + timeout: 10000 + label: "Scroll to Add shipping" + +- tapOn: + id: "add_shipping_button" + retryTapIfNoChange: true + label: "Open Add shipping" + +- extendedWaitUntil: + visible: + id: "order_shipping_amount_input" + timeout: 15000 + +- tapOn: + id: "order_shipping_amount_input" +- inputText: "500" +- hideKeyboard + +- tapOn: + id: "order_shipping_name_input" +- inputText: Maestro shipping ${SUITE_RUN_ID} +- hideKeyboard + +- tapOn: + id: "order_shipping_save_button" + retryTapIfNoChange: true + label: "Save shipping line" + +- extendedWaitUntil: + visible: "New order" + timeout: 15000 + +- assertVisible: + text: "Maestro shipping ${SUITE_RUN_ID}" + label: "Shipping line is present in the order" + +- takeScreenshot: order_create_with_shipping + +# The first shipping line can trigger a feedback overlay that intercepts the +# customer and note controls underneath it. +- runFlow: + when: + visible: "Shipping added!" + commands: + - tapOn: + text: "Close" + label: "Dismiss shipping feedback" + - extendedWaitUntil: + notVisible: "Shipping added!" + timeout: 5000 + +# ── Add a customer-facing note ──────────────────────────────────────── +# Added BEFORE the customer on purpose: when both customer and note are +# empty, the "Add note" button lives in customer_section (via +# initCustomerAndNotesEmptySection). Once a customer is added, that +# button migrates to notes_section whose bounds overlap with the Compose +# totals_section, and the totals overlay intercepts taps on it. +- scrollUntilVisible: + element: "Add note" + direction: DOWN + centerElement: true + timeout: 10000 + label: "Scroll to notes button in customer section" + +- tapOn: + text: "Add note" + childOf: + id: "customer_section" + retryTapIfNoChange: true + label: "Tap Add note in empty customer section" + +- extendedWaitUntil: + visible: + id: "customerOrderNote_editor" + timeout: 15000 + +- tapOn: + id: "customerOrderNote_editor" +- inputText: "Maestro smoke test note - please gift wrap." +- hideKeyboard + +- takeScreenshot: order_create_customer_note_typed + +# Save via toolbar DONE +- tapOn: + id: "menu_done" + label: "Save customer note" + +- extendedWaitUntil: + visible: "New order" + timeout: 15000 + +- assertVisible: + text: ".*Maestro smoke test note.*" + label: "Customer note visible in creation screen" + +# ── Add a customer ──────────────────────────────────────────────────── +# Select an existing customer from the live store. Manual customer entry does +# not satisfy the P2 existing-customer check. +- scrollUntilVisible: + element: "Add customer details" + direction: DOWN + centerElement: true + timeout: 10000 + label: "Scroll to customer section" + +- tapOn: + text: "Add customer details" + label: "Tap Add customer details" + +- extendedWaitUntil: + visible: "Search for customers" + timeout: 15000 + +- extendedWaitUntil: + visible: + id: "customer_list_item" + timeout: 20000 + label: "Require at least one existing customer" + +# Capture and tap the second visible email. Some stores expose an address-only +# first customer whose list summary has an email but whose full customer fetch +# does not; requiring another live row keeps the draft identity assertion real. +- copyTextFrom: + text: ".*@.*" + index: 1 +- evalScript: ${output.selectedCustomerEmail = maestro.copiedText.trim()} +- evalScript: | + if (!output.selectedCustomerEmail || output.selectedCustomerEmail.indexOf('@') < 1) { + throw new Error('Existing customer prerequisite requires at least two customers with email addresses'); + } + +- tapOn: + text: ".*@.*" + index: 1 + retryTapIfNoChange: true + label: "Select existing customer" + +- extendedWaitUntil: + visible: "New order" + timeout: 15000 + +- scrollUntilVisible: + element: "Edit customer details" + direction: DOWN + centerElement: true + timeout: 10000 + label: "Verify the existing customer is attached" + +- takeScreenshot: order_create_with_existing_customer + +# ── Edit the added customer ─────────────────────────────────────────── +- scrollUntilVisible: + element: "Edit customer details" + direction: DOWN + centerElement: true + timeout: 10000 + label: "Scroll to customer section" + +- tapOn: + text: "Edit customer details" + label: "Tap customer edit button" + +- extendedWaitUntil: + visible: ".*Last name.*" + timeout: 15000 + +- assertVisible: + text: "${output.selectedCustomerEmail}" + label: "Customer editor shows the selected live identity" + +# Rewrite the last name with a per-suite-run-unique marker so +# orders_refund.yaml can find THIS exact order and won't pick up an +# already-refunded leftover from a previous suite run. SUITE_RUN_ID is +# exported by .maestro/scripts/run-smoke-tests.sh (set to the run's +# timestamp, e.g. 20260421-123100), so every run's customer last name +# is distinct. +- tapOn: + id: "edit_text" + childOf: + id: "last_name" +- eraseText +- inputText: "SmokeTest-${SUITE_RUN_ID}" +- hideKeyboard + +- takeScreenshot: order_create_customer_re_edited + +- tapOn: + id: "menu_done" + label: "Save customer edit" + +- extendedWaitUntil: + visible: "New order" + timeout: 15000 + +# The order summary currently omits the name/email block after this edit even +# though the draft retains both values. Reopen the editor to verify the actual +# draft model before submitting it. +- scrollUntilVisible: + element: "Edit customer details" + direction: DOWN + centerElement: true + timeout: 10000 +- tapOn: + text: "Edit customer details" + retryTapIfNoChange: true + label: "Reopen customer editor to verify draft persistence" +- extendedWaitUntil: + visible: ".*SmokeTest-${SUITE_RUN_ID}.*" + timeout: 15000 + label: "Edited customer marker is retained by the draft" +- assertVisible: + text: "${output.selectedCustomerEmail}" + label: "Selected customer identity is retained by the draft" +- back +- extendedWaitUntil: + visible: "New order" + timeout: 15000 + +- takeScreenshot: order_create_fully_customized + +# ── Submit + auto-launch payment flow ───────────────────────────────── +# The Compose totals overlay at the bottom of the creation screen +# hosts a "Collect Payment" primary button (see +# OrderCreateEditTotalsHelper.toButtonText → Creation branch). Tapping +# it submits the order AND sets startPaymentFlow=true, so +# OrderDetailViewModel's init auto-triggers StartPaymentFlow — +# landing on the Take-payment selector without requiring a second tap +# on order detail. +- scrollUntilVisible: + element: "Collect Payment" + direction: DOWN + timeout: 10000 + label: "Scroll to Collect Payment primary button" + +- tapOn: + text: "Collect Payment" + retryTapIfNoChange: true + label: "Tap Collect Payment to submit + start payment flow" + +# Take-payment bottom sheet appears (Card/Cash options). +- extendedWaitUntil: + visible: ".*Take payment.*|.*Cash.*" + timeout: 30000 + +- takeScreenshot: order_create_select_payment_method + +- tapOn: + text: "Cash" + retryTapIfNoChange: true + label: "Select Cash payment method" + +# Change Due Calculator — the order total is pre-filled, so tapping +# "Mark Order as Complete" records the cash payment and closes out the +# order. +- extendedWaitUntil: + visible: ".*Mark Order as Complete.*" + timeout: 15000 + +- takeScreenshot: order_create_change_due_calculator + +- tapOn: + text: ".*Mark Order as Complete.*" + retryTapIfNoChange: true + label: "Mark order as complete with cash" + +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 30000 + +- takeScreenshot: order_create_completed + +# Locate the exact completed order and prove the order-local customer edit was +# persisted by the backend, rather than only rendered in the draft. +- tapOn: + id: "menu_search" + label: "Search for the completed smoke order" +- extendedWaitUntil: + visible: + id: "search_src_text" + timeout: 10000 +- tapOn: + id: "search_src_text" +- inputText: SmokeTest-${SUITE_RUN_ID} +- pressKey: Enter +- extendedWaitUntil: + visible: ".*SmokeTest-${SUITE_RUN_ID}.*" + timeout: 30000 + label: "Require the persisted order customer marker" +- tapOn: + id: "orderNum" + index: 0 + retryTapIfNoChange: true + label: "Open the completed smoke order" +- extendedWaitUntil: + visible: + id: "orderDetail_container" + timeout: 20000 +- scrollUntilVisible: + element: + id: "productInfo_name" + direction: DOWN + timeout: 15000 + label: "Find persisted order line items" +- assertVisible: + id: "productInfo_name" + index: 0 + label: "First line item persisted on the completed order" +- assertVisible: + id: "productInfo_name" + index: 1 + label: "Second line item persisted on the completed order" +- assertVisible: + text: ".*⦁.*" + label: "Persisted order retains the selected variation attributes" +- scrollUntilVisible: + element: ".*SmokeTest-${SUITE_RUN_ID}.*" + direction: DOWN + timeout: 15000 + label: "Verify the edited marker on persisted order details" + +# Return to a fresh draft, reopen the customer picker, and search by the +# captured email. The original name/email must still be present, proving the +# arbitrary live customer account was not modified by the order edit. +- back +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 15000 +- tapOn: + id: "createOrderButton" + retryTapIfNoChange: true + label: "Open a fresh order for customer integrity check" +- scrollUntilVisible: + element: "Add customer details" + direction: DOWN + centerElement: true + timeout: 15000 + label: "Reach customer picker in fresh order" +- tapOn: + text: "Add customer details" + retryTapIfNoChange: true +- extendedWaitUntil: + visible: "Search for customers" + timeout: 15000 +- tapOn: "Search for customers" +- inputText: ${output.selectedCustomerEmail} +- hideKeyboard +- extendedWaitUntil: + visible: "${output.selectedCustomerEmail}" + timeout: 20000 + label: "Find the original customer after order creation" +- copyTextFrom: + text: "${output.selectedCustomerEmail}" +- evalScript: | + if (maestro.copiedText.trim() !== output.selectedCustomerEmail) { + throw new Error('Store customer email changed after editing the order-local copy'); + } +- assertNotVisible: + text: ".*SmokeTest-${SUITE_RUN_ID}.*" + label: "Order-local marker was not persisted to the store customer" +- back +- back +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 15000 + +- takeScreenshot: order_create_customer_integrity_verified diff --git a/.maestro/flows/orders_details_and_actions.yaml b/.maestro/flows/orders_details_and_actions.yaml new file mode 100644 index 000000000000..30712ff8e7a4 --- /dev/null +++ b/.maestro/flows/orders_details_and_actions.yaml @@ -0,0 +1,175 @@ +# Smoke Test: Orders - Detail view and actions +# p2: orders.receipt, orders.note +# P2 ref: Orders > Refund, Add order note, Mark complete, See receipt +# +# Verifies: +# - Opening an order shows the detail container and status tags +# - Can scroll through products and payment sections +# - Can add an order note via the "Add a note" row (private by default) +# - Returns to orders list cleanly +# +# Uses the exact resource IDs from order_detail_note_list.xml +# (noteList_addNoteContainer, notesList_notes) and menu_add.xml +# (menu_add) to avoid ambiguous text matches that can collide with the +# "Add note" button in the order creation flow. +appId: com.woocommerce.android.dev +name: "Orders - Detail view and actions" +tags: + - smoke_extended + - flaky_quarantine + - orders + - destructive +--- +# Start from an already-authenticated session (see comment in +# ensure_logged_in.yaml — re-logging in on every run triggers WPCom +# security screens). +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +# Navigate to Orders +- runFlow: + file: ../subflows/navigate_to_orders.yaml + +# Open one of this run's seeded orders — never an arbitrary first row. +# This flow persists a note on the order it opens, and on the shared +# store index-0 can be a manual tester's real order. Searching the +# suite run id scopes the list to automation-owned fixtures only. +- tapOn: + id: "menu_search" + label: "Open order search" + +- extendedWaitUntil: + visible: + id: "search_src_text" + timeout: 10000 + +- tapOn: + id: "search_src_text" +- inputText: ${SUITE_RUN_ID} +- pressKey: Enter + +- extendedWaitUntil: + visible: + id: "orderNum" + timeout: 20000 + label: "Wait for seeded orders in results" + +- tapOn: + id: "orderNum" + index: 0 + label: "Open first seeded order" + +# Wait for order detail +- extendedWaitUntil: + visible: + id: "orderDetail_container" + timeout: 15000 + +# Verify order detail elements +- assertVisible: + id: "orderStatus_orderTags" + label: "Order status tags are visible" + +- takeScreenshot: order_detail_top + +# Scroll to the products section. A plain `scroll` can overshoot on +# taller devices and land in payment totals before this assertion runs. +- scrollUntilVisible: + element: + id: "productInfo_name" + direction: DOWN + timeout: 15000 + label: "Scroll to product row in order detail" + +- takeScreenshot: order_detail_products + +# Scroll to payment info +- scrollUntilVisible: + element: + id: "paymentInfo_total" + direction: DOWN + timeout: 15000 + label: "Scroll to payment total" + +- takeScreenshot: order_detail_payment + +# ── See receipt ─────────────────────────────────────────────────────── +- scrollUntilVisible: + element: "See receipt" + direction: DOWN + timeout: 15000 + label: "Scroll to See receipt" + +- tapOn: + text: "See receipt" + retryTapIfNoChange: true + label: "Open receipt preview" + +- extendedWaitUntil: + visible: ".*Receipt Preview.*|.*Print.*|.*Send.*" + timeout: 30000 + label: "Wait for receipt preview" + +- takeScreenshot: order_receipt_preview +- back + +- extendedWaitUntil: + visible: + id: "orderDetail_container" + timeout: 15000 + +# ── Add an order note ──────────────────────────────────────────────── +# Use the addNoteContainer ID directly — more robust than matching the +# "Add a note" text which is not tagged as clickable (the outer +# LinearLayout handles the click). +- scrollUntilVisible: + element: + id: "noteList_addNoteContainer" + direction: DOWN + timeout: 15000 + label: "Scroll to notes section" + +- tapOn: + id: "noteList_addNoteContainer" + retryTapIfNoChange: true + label: "Tap Add a note row" + +# Add note screen +- extendedWaitUntil: + visible: + id: "addNote_editor" + timeout: 15000 + +- tapOn: + id: "addNote_editor" +- inputText: "Maestro smoke test note" +- hideKeyboard + +- takeScreenshot: order_note_typed + +# Save the note via the Add menu item +- tapOn: + id: "menu_add" + label: "Save order note" + +# Back on order detail — verify note appears in the notes list +- extendedWaitUntil: + visible: + id: "notesList_notes" + timeout: 15000 + +- scrollUntilVisible: + element: ".*Maestro smoke test note.*" + direction: DOWN + timeout: 15000 + label: "Scroll to added note" + +- takeScreenshot: order_note_added + +# Go back to orders list +- back + +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 10000 diff --git a/.maestro/flows/orders_list_and_search.yaml b/.maestro/flows/orders_list_and_search.yaml new file mode 100644 index 000000000000..54b162a554b2 --- /dev/null +++ b/.maestro/flows/orders_list_and_search.yaml @@ -0,0 +1,74 @@ +# Smoke Test: Orders - List and Search +# p2: orders.list, orders.search +# P2 ref: Orders > Order list, Search for an order +# +# Verifies: +# - Orders list loads with order data +# - Search for an order by number or keyword +appId: com.woocommerce.android.dev +name: "Orders - List and search" +tags: + - smoke_core + - orders +--- +# Reuse the logged-in session — login flows run first and leave the +# app authenticated. See subflows/ensure_logged_in.yaml. +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +# Navigate to Orders +- runFlow: + file: ../subflows/navigate_to_orders.yaml + +# Verify orders list loaded +- assertVisible: + id: "ordersList" + label: "Orders list is visible" + +- assertVisible: + id: "orderNum" + label: "At least one order number is visible" + +# Validate first order has a non-empty number via JavaScript +- copyTextFrom: + id: "orderNum" + index: 0 +- evalScript: | + var text = maestro.copiedText; + if (!text || text.trim() === '') { + throw new Error('Order number text is empty'); + } +- evalScript: ${output.orderSearchText = maestro.copiedText.trim()} + +- takeScreenshot: orders_list + +# Test search against the first visible order from the loaded list. +- tapOn: + id: "menu_search" + label: "Open order search" + +- extendedWaitUntil: + visible: + id: "search_src_text" + timeout: 10000 + +- tapOn: + id: "search_src_text" +- inputText: ${output.orderSearchText} +- pressKey: Enter + +# Wait for search results +- extendedWaitUntil: + visible: + id: "orderNum" + timeout: 15000 + +- assertVisible: + text: "${output.orderSearchText}" + label: "Captured order appears in search results" + +- takeScreenshot: orders_search_results + +# Go back from search +- back +- back diff --git a/.maestro/flows/orders_mark_complete.yaml b/.maestro/flows/orders_mark_complete.yaml new file mode 100644 index 000000000000..5588c458e94c --- /dev/null +++ b/.maestro/flows/orders_mark_complete.yaml @@ -0,0 +1,203 @@ +# Smoke Test: Orders - Mark order complete +# p2: orders.mark-complete +# P2 ref: Orders > Mark complete +# +# Verifies: +# - Create a fresh paid automation order through the app UI +# - Move that order back to Processing via the order status editor +# - Navigate to the Fulfill screen via "Mark order complete" +# - Complete the fulfillment +# - "🎉 Order completed!" snackbar appears +# +# The Mark Complete button is only exposed for paid orders whose status +# is not Completed. This flow creates its own paid order, moves that +# automation-owned order to Processing, then marks it Completed again. +appId: com.woocommerce.android.dev +name: "Orders - Mark order complete" +tags: + - smoke_extended + - flaky_quarantine + - orders + - destructive +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/navigate_to_orders.yaml + +# ── Create a paid automation order ─────────────────────────────────── +- tapOn: + id: "createOrderButton" + retryTapIfNoChange: true + label: "Tap Create Order FAB" + +- extendedWaitUntil: + visible: "Add products" + timeout: 15000 + +- tapOn: + text: "Add products" + label: "Tap Add products" + +- extendedWaitUntil: + visible: ".*Search Products.*" + timeout: 15000 + +- tapOn: + point: "50%,30%" + label: "Tap first product row" + +- extendedWaitUntil: + visible: ".*Select \\d+ Product.*" + timeout: 15000 + +- tapOn: + text: ".*Select \\d+ Product.*" + label: "Confirm product selection" + +- extendedWaitUntil: + visible: "New order" + timeout: 15000 + +- scrollUntilVisible: + element: "Collect Payment" + direction: DOWN + timeout: 10000 + label: "Scroll to Collect Payment primary button" + +- tapOn: + text: "Collect Payment" + retryTapIfNoChange: true + label: "Submit order and start payment flow" + +- extendedWaitUntil: + visible: ".*Take payment.*|.*Cash.*" + timeout: 30000 + +- tapOn: + text: "Cash" + retryTapIfNoChange: true + label: "Select Cash payment method" + +- extendedWaitUntil: + visible: ".*Mark Order as Complete.*" + timeout: 15000 + +- tapOn: + text: ".*Mark Order as Complete.*" + retryTapIfNoChange: true + label: "Create paid completed order" + +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 30000 + +- takeScreenshot: order_mark_complete_paid_order_created + +# Reopen the newest order this flow just created. It is automation-owned +# and appears at the top of the list after the cash completion above. +- tapOn: + id: "orderNum" + index: 0 + retryTapIfNoChange: true + label: "Open newly-created completed order" + +- extendedWaitUntil: + visible: + id: "orderDetail_container" + timeout: 15000 + +- takeScreenshot: order_detail_completed_before_status_edit + +# ── Move the paid order back to Processing ─────────────────────────── +- tapOn: + id: "orderStatus_container" + retryTapIfNoChange: true + label: "Open order status selector" + +- extendedWaitUntil: + visible: "Change order status" + timeout: 10000 + +- tapOn: + text: "Processing" + label: "Select Processing status" + +- tapOn: + text: "Apply" + retryTapIfNoChange: true + label: "Apply Processing status" + +- extendedWaitUntil: + visible: ".*Processing.*" + timeout: 30000 + label: "Wait for Processing status" + +- takeScreenshot: order_detail_processing + +# ── Mark complete → Fulfill screen ─────────────────────────────────── +- scrollUntilVisible: + element: + id: "productList_btnMarkOrderComplete" + direction: DOWN + timeout: 15000 + label: "Scroll to Mark order complete button" + +- tapOn: + id: "productList_btnMarkOrderComplete" + retryTapIfNoChange: true + label: "Tap Mark order complete" + +# Fulfill screen has its own Mark Complete CTA that actually commits +# the status change. +- extendedWaitUntil: + visible: + id: "button_mark_order_compete" + timeout: 15000 + +- takeScreenshot: order_fulfill_screen + +- tapOn: + id: "button_mark_order_compete" + retryTapIfNoChange: true + label: "Confirm mark complete" + +# ── Verify completion ──────────────────────────────────────────────── +# On stores with shipping labels enabled, the Fulfill confirm can race +# into an auto-navigation to the Create Shipping Label screen. Back out +# until the orders list reappears, which is this flow's final state. +- runFlow: + when: + visible: ".*Create shipping label.*" + commands: + - back + # Create Shipping Label is a sibling of Order Detail in the + # nav graph — a single back returns directly to the orders + # list on this store config. + +# If we're still on order detail, back out to the orders list. +- runFlow: + when: + visible: + id: "orderDetail_container" + commands: + - extendedWaitUntil: + visible: ".*Completed.*|.*Order completed.*|.*Undo.*" + timeout: 15000 + label: "Verify completed status or snackbar" + - takeScreenshot: order_marked_complete + - back + +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 15000 + +- takeScreenshot: order_marked_complete_list + +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 10000 diff --git a/.maestro/flows/orders_payment_qr_and_share.yaml b/.maestro/flows/orders_payment_qr_and_share.yaml new file mode 100644 index 000000000000..7252bf503f86 --- /dev/null +++ b/.maestro/flows/orders_payment_qr_and_share.yaml @@ -0,0 +1,186 @@ +# Smoke Test: Orders - Payment QR and share link +# p2: orders.collect-payment.qr, orders.collect-payment.share-link +# P2 ref: Orders > Collect payment using QR code / Share payment link +# +# Verifies: +# - A fresh UI-created pending order exposes Collect Payment +# - Scan To Pay renders its QR instructions +# - Share Payment Link opens the Android share sheet +# - The flow backs out without collecting payment +appId: com.woocommerce.android.dev +name: "Orders - Payment QR and share link" +tags: + - smoke_extended + - flaky_quarantine + - orders + - destructive + - system_surface +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/navigate_to_orders.yaml + +# Create a fresh pending-payment order owned by this flow. This avoids opening +# an arbitrary shared-store order whose payment state may have changed. +- tapOn: + id: "createOrderButton" + retryTapIfNoChange: true + label: "Tap Create Order FAB" + +- extendedWaitUntil: + visible: "Add products" + timeout: 15000 + +- tapOn: + text: "Add products" + label: "Tap Add products" + +- extendedWaitUntil: + visible: ".*Search Products.*" + timeout: 15000 + +- extendedWaitUntil: + visible: + id: "product_selector_product_item" + timeout: 15000 + label: "Require a selectable product" + +- tapOn: + id: "product_selector_product_item" + index: 0 + retryTapIfNoChange: true + label: "Select first available product" + +- runFlow: + when: + visible: + id: "product_selector_variation_item" + commands: + - tapOn: + id: "product_selector_variation_item" + index: 0 + label: "Select first available variation" + - back + +- extendedWaitUntil: + visible: + id: "product_selector_done_button" + timeout: 15000 + label: "Wait for product selection" + +- tapOn: + id: "product_selector_done_button" + label: "Confirm product selection" + +- extendedWaitUntil: + visible: "New order" + timeout: 15000 + +- assertVisible: + id: "order_product_card" + label: "Selected product is present in the order" + +- tapOn: + id: "menu_create" + retryTapIfNoChange: true + label: "Create pending-payment order" + +- extendedWaitUntil: + visible: + id: "orderDetail_container" + timeout: 15000 + +- extendedWaitUntil: + visible: ".*Pending payment.*" + timeout: 20000 + label: "Wait for pending-payment order detail" + +- takeScreenshot: order_payment_surface_pending_detail + +- scrollUntilVisible: + element: + id: "paymentInfo_collectCardPresentPaymentButton" + direction: DOWN + timeout: 15000 + label: "Scroll to Collect Payment button" + +- tapOn: + id: "paymentInfo_collectCardPresentPaymentButton" + retryTapIfNoChange: true + label: "Tap Collect Payment" + +- runFlow: + when: + notVisible: ".*Take payment.*|.*Cash.*" + commands: + - extendedWaitUntil: + visible: ".*Take payment.*|.*Cash.*" + timeout: 10000 + +- runFlow: + when: + notVisible: ".*Take payment.*|.*Cash.*" + commands: + - tapOn: + id: "paymentInfo_collectCardPresentPaymentButton" + retryTapIfNoChange: true + label: "Retry Collect Payment tap" + +- extendedWaitUntil: + visible: ".*Take payment.*|.*Cash.*" + timeout: 30000 + +- takeScreenshot: order_payment_surface_method_list + +- scrollUntilVisible: + element: "Scan To Pay" + direction: DOWN + timeout: 10000 + label: "Require Scan To Pay" + +- tapOn: + text: "Scan To Pay" + retryTapIfNoChange: true + label: "Open Scan To Pay QR" + +- extendedWaitUntil: + visible: "Scan QR and follow instructions" + timeout: 30000 + label: "Wait for Scan To Pay QR" + +- takeScreenshot: order_payment_scan_to_pay_qr + +# Back dismisses the QR without reporting payment completion. +- back + +- extendedWaitUntil: + visible: ".*Take payment.*|.*Cash.*" + timeout: 15000 + +- scrollUntilVisible: + element: "Share Payment Link" + direction: DOWN + timeout: 10000 + label: "Require Share Payment Link" + +- tapOn: + text: "Share Payment Link" + retryTapIfNoChange: true + label: "Open Android share sheet" + +- extendedWaitUntil: + visible: ".*Share with.*|.*Copy.*|.*Messages.*|.*Quick Share.*|.*Checkout.*" + timeout: 15000 + label: "Wait for Android share sheet" + +- takeScreenshot: order_payment_share_sheet + +- back + +- extendedWaitUntil: + visible: + id: "orderDetail_container" + timeout: 30000 + label: "Wait for order detail after dismissing share sheet" diff --git a/.maestro/flows/orders_refund.yaml b/.maestro/flows/orders_refund.yaml new file mode 100644 index 000000000000..c41d2d5a49e1 --- /dev/null +++ b/.maestro/flows/orders_refund.yaml @@ -0,0 +1,259 @@ +# Smoke Test: Orders - Issue refund +# p2: orders.refund +# P2 ref: Orders > Refund +# +# Verifies: +# - Filter the orders list by "Completed" status (the Refund button +# in the payment section is only exposed once an order has been +# transitioned to Completed — Processing orders do not show it +# on this app) +# - Open a completed order +# - Tap "Refund" in the payment info section +# - Select all items on the Issue Refund screen +# - Advance to the Refund Summary screen +# - Enter a refund reason +# - Tap the Refund CTA to reach the confirmation dialog +# - Confirm the refund by tapping the positive "Refund" button, +# which actually hits the WPCom refund endpoint +# - Wait for the success snackbar ("The refund was successfully +# submitted.") and verify we land back on order detail with the +# refund recorded +# +# Unlike the mark-complete flow, the refund is fully committed — the +# staging store will accumulate refunded orders across runs. Test +# orders on the store are priced low enough that this is acceptable. +appId: com.woocommerce.android.dev +name: "Orders - Issue refund" +tags: + - smoke_extended + - flaky_quarantine + - orders + - destructive +--- +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/navigate_to_orders.yaml + +# ── Filter by Completed status ──────────────────────────────────────── +# The Refund button in the payment info section is only exposed on +# orders that have been transitioned to Completed. Processing-status +# orders — even those paid via gateways that support refunds — do not +# show the button on this app. Clear any previously-applied filters +# first to avoid cross-test contamination. +- tapOn: + id: "btn_order_filter" + label: "Open filters" + +- runFlow: + when: + visible: ".*Clear.*" + commands: + - tapOn: + text: ".*Clear.*" + label: "Clear existing filters" + +- extendedWaitUntil: + visible: "Order Status" + timeout: 10000 + +- tapOn: + text: "Order Status" + label: "Open status filter" + +- extendedWaitUntil: + visible: ".*Completed.*" + timeout: 10000 + +- tapOn: + text: ".*Completed.*" + label: "Select Completed status" + +- tapOn: + id: "showOrdersButton" + label: "Apply filter" + +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 15000 + +# Pull-to-refresh so the status filter reflects the latest server +# state. Without this, the cached list can still contain an order +# that was just refunded in a previous run — the refund button won't +# exist on it once the refund API has reclassified it as Refunded, +# and the scrollUntilVisible below would fail. +- swipe: + from: + id: "ordersList" + direction: DOWN + duration: 800 + +# ── Open the freshly-created order (suite) or first completed order ── +# In a full-suite run, orders_create.yaml stamps the created order's +# customer last name with the suite's unique run ID (SUITE_RUN_ID, +# e.g. "SmokeTest-20260421-123100") so we can target that exact order +# here instead of a leftover "SmokeTest*" customer from a previous +# suite run — those prior orders will have already been fully +# refunded and show 0 refundable items on the Issue Refund screen. +# Fall back to this run's seeded refundable fixture (its billing name +# is the run id) when the suite-tagged customer isn't visible (e.g. +# this flow is run standalone). Never fall back to an arbitrary first +# row — on the shared store that could issue a refund against a manual +# tester's real order; if neither automation order is visible, failing +# here is the correct outcome. +- runFlow: + when: + visible: ".*SmokeTest-${SUITE_RUN_ID}.*" + commands: + - tapOn: + text: ".*SmokeTest-${SUITE_RUN_ID}.*" + retryTapIfNoChange: true + label: "Open this-run's SmokeTest order" + - extendedWaitUntil: + visible: + id: "orderDetail_container" + timeout: 15000 + label: "Wait for SmokeTest order detail" + +- runFlow: + when: + notVisible: + id: "orderDetail_container" + commands: + - tapOn: + text: ".*${SUITE_RUN_ID}.*" + retryTapIfNoChange: true + label: "Open this run's seeded refundable order (fallback)" + +- extendedWaitUntil: + visible: + id: "orderDetail_container" + timeout: 15000 + +# ── Refund → Issue Refund screen ───────────────────────────────────── +- scrollUntilVisible: + element: + id: "paymentInfo_issueRefundButton" + direction: DOWN + timeout: 15000 + label: "Scroll to Refund button" + +- tapOn: + id: "paymentInfo_issueRefundButton" + retryTapIfNoChange: true + label: "Tap Refund" + +- extendedWaitUntil: + visible: + id: "issueRefund_btnNextFromItems" + timeout: 15000 + +- takeScreenshot: issue_refund_items + +# Select all items +- tapOn: + id: "issueRefund_selectButton" + retryTapIfNoChange: true + label: "Select all items" + +- tapOn: + id: "issueRefund_btnNextFromItems" + retryTapIfNoChange: true + label: "Next to refund summary" + +# ── Refund Summary ─────────────────────────────────────────────────── +- extendedWaitUntil: + visible: + id: "refundSummary_btnRefund" + timeout: 15000 + +- takeScreenshot: refund_summary + +# Enter a reason so the "Refund" CTA is verifiably exercised with a +# filled-in reason field. +- tapOn: + id: "refundSummary_reason" +- inputText: "Maestro smoke test refund" +- hideKeyboard + +- takeScreenshot: refund_summary_with_reason + +- tapOn: + id: "refundSummary_btnRefund" + retryTapIfNoChange: true + label: "Tap Refund (opens confirmation dialog)" + +# ── Confirmation dialog — confirm to submit the refund ─────────────── +- extendedWaitUntil: + visible: ".*Are you sure you want to issue a refund.*" + timeout: 15000 + +- takeScreenshot: refund_confirmation_dialog + +# The MaterialAlertDialog positive button is the standard Android +# `button1` resource ID, with label "Refund" (from +# R.string.order_refunds_refund). Tapping it fires onRefundConfirmed +# → initiateRefund → WPCom POST /wc/v3/orders/{id}/refunds. +- tapOn: + id: "button1" + retryTapIfNoChange: true + label: "Confirm refund" + +# ── Verify the refund succeeded ────────────────────────────────────── +# On success the ViewModel shows a transient snackbar ("The refund was +# successfully submitted.") and pops the back stack past the +# IssueRefund fragment, landing on order detail with a "Refunded" +# line recorded in the payment totals section. The snackbar +# auto-dismisses in ~3-5s which is often faster than Maestro can +# observe reliably, so the durable assertion is the post-refund state: +# we're back on orderDetail_container AND a Refunded line is present. +- runFlow: + when: + visible: ".*The refund was successfully submitted.*" + commands: + - takeScreenshot: refund_success_snackbar + +- extendedWaitUntil: + visible: + id: "orderDetail_container" + timeout: 20000 + +- scrollUntilVisible: + element: ".*Refunded.*" + direction: DOWN + timeout: 15000 + label: "Scroll to Refunded line in payment totals" + +- takeScreenshot: order_detail_after_refund + +# ── Back out to orders list ────────────────────────────────────────── +- back + +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 10000 + +# ── Clear the filter so future runs start from an unfiltered list ──── +- tapOn: + id: "btn_order_filter" + label: "Re-open filters to clear" + +- runFlow: + when: + visible: ".*Clear.*" + commands: + - tapOn: + text: ".*Clear.*" + label: "Clear filters" + +- tapOn: + id: "showOrdersButton" + label: "Apply cleared filter" + +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 10000 diff --git a/.maestro/flows/pos_cash_payment.yaml b/.maestro/flows/pos_cash_payment.yaml new file mode 100644 index 000000000000..ccd8783631d5 --- /dev/null +++ b/.maestro/flows/pos_cash_payment.yaml @@ -0,0 +1,104 @@ +# Smoke Test: POS - Cash Payment flow (Tablet) +# p2: pos.add-products, pos.pay-cash +# P2 ref: POS > Search products, Add to cart, Pay with Cash +# +# POS exposes Compose test tags as resource IDs (WooPosTheme enables +# testTagsAsResourceId at the root of the Compose tree). +# +# NOTE: This flow requires a tablet device/emulator and a genuinely POS- +# eligible store. Missing POS navigation must fail the release signal. +# +# Verifies (on tablet): +# - POS screen loads from bottom navigation +# - Products load and can be added to cart +# - Checkout flow works +# - Cash payment can be completed +# - Payment success screen displays +# - A new order can be started after successful payment +appId: com.woocommerce.android.dev +name: "POS - Cash payment flow" +tags: + - pos_tablet + - flaky_quarantine + - destructive +--- +# Reuse the logged-in session — login flows run first and leave the +# app authenticated. See subflows/ensure_logged_in.yaml. +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- extendedWaitUntil: + visible: + id: "point_of_sale" + timeout: 10000 + label: "Require real POS eligibility" + +- tapOn: + id: "point_of_sale" + label: "Tap POS tab" + +- extendedWaitUntil: + visible: + id: "woo_pos_product_item" + timeout: 20000 + +- runFlow: + when: + visible: + id: "woo_pos_clear_cart_button" + commands: + - tapOn: + id: "woo_pos_clear_cart_button" + label: "Open clear-cart menu" + - tapOn: + text: "Clear cart" + label: "Own an empty POS cart" + +- runFlow: + file: ../subflows/pos_add_discovered_variable_product.yaml + +- copyTextFrom: + id: "woo_pos_cart_items_count" +- evalScript: | + var count = parseInt(maestro.copiedText, 10); + if (isNaN(count) || count < 1) { + throw new Error('Owned POS cart should contain a product'); + } + +- extendedWaitUntil: + visible: + id: "woo_pos_checkout_button" + timeout: 10000 +- tapOn: + id: "woo_pos_checkout_button" + label: "Tap Checkout" + +- extendedWaitUntil: + visible: + id: "woo_pos_cash_payment_button" + timeout: 15000 +- tapOn: + id: "woo_pos_cash_payment_button" + label: "Select Cash payment" + +- extendedWaitUntil: + visible: + id: "woo_pos_complete_payment_button" + timeout: 10000 +- tapOn: + id: "woo_pos_complete_payment_button" + label: "Complete payment" + +- extendedWaitUntil: + visible: + id: "woo_pos_new_order_button" + timeout: 15000 +- assertVisible: + id: "woo_pos_success_checkmark_icon" + label: "Payment success checkmark visible" + +- takeScreenshot: pos_payment_success + +- tapOn: + id: "woo_pos_new_order_button" + label: "Start new order" diff --git a/.maestro/flows/pos_search_and_coupons.yaml b/.maestro/flows/pos_search_and_coupons.yaml new file mode 100644 index 000000000000..50aada26459b --- /dev/null +++ b/.maestro/flows/pos_search_and_coupons.yaml @@ -0,0 +1,109 @@ +# Smoke Test: POS - Search + Coupons tab (Tablet) +# p2: pos.search-products, pos.coupons +# P2 ref: POS > Search products, Use coupons +# +# POS exposes the Products/Coupons tabs as plain Compose text. The shared +# WooPosSearchInput exposes a stable resource ID and the hint strings tell +# which mode it is in: +# - Products tab: hint is R.string.woopos_search_products_and_variations +# → "Search products and variations" +# - Coupons tab: hint is R.string.woopos_search_coupons +# → "Search coupons" +# +# NOTE: POS is tablet-only and requires a genuinely eligible store. Missing +# POS navigation is a setup failure, not a reason to skip this release signal. +# +# Verifies (on tablet): +# - POS screen loads with Products + Coupons tabs visible +# - Search resolves a variable product discovered from the live catalog +# - A real coupon is added to the cart +# - Switching back to Products reloads the product grid +appId: com.woocommerce.android.dev +name: "POS - Search and Coupons tab" +tags: + - pos_tablet + - flaky_quarantine +--- +# Reuse the logged-in session — login flows run first and leave the +# app authenticated. See subflows/ensure_logged_in.yaml. +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- extendedWaitUntil: + visible: + id: "point_of_sale" + timeout: 10000 + label: "Require real POS eligibility" + +- tapOn: + id: "point_of_sale" + label: "Tap POS tab" + +- extendedWaitUntil: + visible: + id: "woo_pos_product_item" + timeout: 20000 + +- runFlow: + when: + visible: + id: "woo_pos_clear_cart_button" + commands: + - tapOn: + id: "woo_pos_clear_cart_button" + label: "Open clear-cart menu" + - tapOn: + text: "Clear cart" + label: "Clear leftover POS cart" + +- assertVisible: "Products" +- assertVisible: "Coupons" +- assertVisible: + id: "woo_pos_search_input" + +- runFlow: + file: ../subflows/pos_add_discovered_variable_product.yaml + +- runFlow: + when: + notVisible: "Coupons" + commands: + - back + +- extendedWaitUntil: + visible: "Coupons" + timeout: 10000 + +- tapOn: + text: "Coupons" + retryTapIfNoChange: true + label: "Tap Coupons tab" + +- extendedWaitUntil: + visible: + id: "woo_pos_coupon_add_to_cart_button" + timeout: 20000 + label: "Require a usable coupon" + +- tapOn: + id: "woo_pos_coupon_add_to_cart_button" + index: 0 + label: "Add coupon to cart" + +- extendedWaitUntil: + visible: + id: "woo_pos_cart_coupon_item" + timeout: 10000 + label: "Verify coupon in POS cart" + +- takeScreenshot: pos_coupon_in_cart + +- tapOn: + text: "Products" + retryTapIfNoChange: true + label: "Restore Products tab" + +- extendedWaitUntil: + visible: + id: "woo_pos_product_item" + timeout: 15000 diff --git a/.maestro/flows/products_create.yaml b/.maestro/flows/products_create.yaml new file mode 100644 index 000000000000..bfd97e78eb51 --- /dev/null +++ b/.maestro/flows/products_create.yaml @@ -0,0 +1,1111 @@ +# Smoke Test: Products - Create and publish a full product +# p2: products.create, products.detail.description, products.detail.price, products.detail.inventory, products.detail.shipping, products.detail.type, products.detail.tags, products.detail.short-description, products.detail.linked, products.detail.downloads +# P2 ref: Products > Create product +# +# Exercises the full happy-path creation flow for a run-owned Simple physical +# product, publishes it, reopens the exact product, and verifies its values: +# +# 1. FAB → Manual → Simple physical product +# 2. Enter a name and main description unique to SUITE_RUN_ID +# 3. Set the regular price +# 4. Configure Inventory with a unique SKU and managed stock quantity +# 5. Add Shipping dimensions and prove the draft is a physical Simple product +# 6. Create and attach a unique tag +# 7. Add an Upsell from Linked products +# 8. Add a downloadable file by URL +# 9. Tap "Add more details" (productDetail_addMoreButton) — on a +# freshly-created simple product the Short description and +# Categories rows don't appear on the detail screen until the +# user opts them in from this bottom sheet +# 10. From the sheet pick "Short description" and enter a unique value +# 11. Tap "Add more details" again → pick "Categories" → ProductCategorySelector +# → select the first category ("Uncategorized" if present) → Done +# 12. Tap PUBLISH in the toolbar +# 13. Search by run ID, reopen the exact product, and verify Description, +# Price, Inventory, Shipping, type, tag, Short description, Upsell, and +# Downloadable file values persisted in their editors +# +# ⚠️ STAGING-STORE MUTATION: this flow publishes a REAL product on +# the staging store every run. Clean up periodically by searching +# "Maestro Smoke" in wp-admin → Products and bulk-trashing the matches. +# A future improvement would be to trash the product automatically at +# the end of the flow (menu_trash_product exists). +# +# Source strings (res/values/strings.xml): +# - product_price_empty → "Add price" +# - product_detail_add_more → "Add more details" +# - product_short_description → "Short description" +# - product_categories → "Categories" +# - product_category_selector_title → "Select categories" +# - product_category_selector_select_button_title_one → "Select 1 category" +# - product_add_tool_bar_menu_button_done → "Publish" +appId: com.woocommerce.android.dev +name: "Products - Create and publish a full product" +tags: + - smoke_extended + - flaky_quarantine + - products + - destructive +--- +# Reuse the logged-in session. +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +- runFlow: + file: ../subflows/ensure_configured_woo_store.yaml + +# Navigate to Products +- runFlow: + file: ../subflows/navigate_to_products.yaml + +# ───────────────────────────────────────────────────────────────────── +# 1. Start product creation +# ───────────────────────────────────────────────────────────────────── +- tapOn: + id: "addProductButton" + retryTapIfNoChange: true + label: "Tap Add Product FAB" + +- extendedWaitUntil: + visible: ".*manual.*|.*Manual.*|.*Select a product type.*|.*Simple physical.*" + timeout: 15000 + label: "Wait for creation entry sheet" + +- runFlow: + when: + visible: ".*manual.*|.*Manual.*" + commands: + - tapOn: ".*manual.*|.*Manual.*" + +- runFlow: + when: + visible: ".*Simple physical product.*" + commands: + - tapOn: ".*Simple physical product.*" + +- extendedWaitUntil: + visible: ".*GOT IT.*|.*Write with AI.*|.*Describe your product.*" + timeout: 20000 + label: "Wait for creation form" + +- runFlow: + when: + visible: "GOT IT" + commands: + - tapOn: "GOT IT" + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + +- takeScreenshot: product_create_empty_form + +# ───────────────────────────────────────────────────────────────────── +# 2. Name — stamped with the suite run id so (a) the end-of-flow search +# matches exactly this run's product, never a leftover duplicate, +# and (b) the stale-orphan sweep can find and delete it if the run +# crashes before any cleanup. +# ───────────────────────────────────────────────────────────────────── +- tapOn: + id: "editText" + label: "Tap product name field" + +- inputText: "Maestro Smoke Product ${SUITE_RUN_ID}" + +- hideKeyboard + +- takeScreenshot: product_create_named + +# Main description — unique to this run so the reopened editor proves the +# published product, rather than a stale list row, retained the mutation. +- tapOn: + text: "Describe your product" + retryTapIfNoChange: true + label: "Open main product description" + +- extendedWaitUntil: + visible: + id: "visualEditor" + timeout: 15000 + +- tapOn: + id: "visualEditor" +- inputText: "Maestro description ${SUITE_RUN_ID}" +- hideKeyboard +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Save main description through the editor toolbar" + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + label: "Return after main description edit" + +- tapOn: + text: "Description" + retryTapIfNoChange: true + label: "Reopen draft main description" +- extendedWaitUntil: + visible: + id: "visualEditor" + timeout: 15000 +- copyTextFrom: + id: "visualEditor" +- evalScript: | + if (!maestro.copiedText || !maestro.copiedText.includes('Maestro description ' + String(SUITE_RUN_ID))) { + throw new Error('Draft product did not retain its main description'); + } +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Return from verified main description" +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + +# ───────────────────────────────────────────────────────────────────── +# 3. Price — "Add price" ComplexProperty → ProductPricingFragment +# ───────────────────────────────────────────────────────────────────── +- scrollUntilVisible: + element: ".*Add price.*|.*Price.*" + direction: DOWN + timeout: 10000 + label: "Scroll to Add price row" + +- tapOn: + text: ".*Add price.*|.*Price.*" + retryTapIfNoChange: true + label: "Tap Add price row" + +- extendedWaitUntil: + visible: + id: "product_regular_price" + timeout: 15000 + label: "Wait for pricing editor" + +- tapOn: + id: "product_regular_price" + label: "Tap regular price field" + +- inputText: "19.99" + +- hideKeyboard + +- takeScreenshot: product_create_price_entered + +# Use the toolbar affordance so ProductPricingFragment delivers its result. +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Save price through the editor toolbar" + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + label: "Wait for detail screen after price edit" + +# ───────────────────────────────────────────────────────────────────── +# 4. Inventory — set a run-unique SKU and enable managed stock +# ───────────────────────────────────────────────────────────────────── +- scrollUntilVisible: + element: "Inventory" + direction: DOWN + timeout: 10000 + label: "Scroll to Inventory" + +- tapOn: + text: "Inventory" + retryTapIfNoChange: true + label: "Open Inventory settings" + +- extendedWaitUntil: + visible: + id: "product_sku" + timeout: 15000 + label: "Wait for Inventory editor" + +- tapOn: + id: "product_sku" + label: "Focus SKU" +- eraseText +- inputText: "SMOKE-${SUITE_RUN_ID}" +- hideKeyboard + +- tapOn: + id: "manageStock_switch" + label: "Enable stock management" + +- extendedWaitUntil: + visible: + id: "product_stock_quantity" + timeout: 10000 + label: "Wait for managed stock fields" + +- tapOn: + id: "product_stock_quantity" + label: "Focus stock quantity" +- eraseText +- inputText: "7" +- hideKeyboard + +- assertVisible: + id: "manageStock_switch" + checked: true + label: "Stock management is enabled" + +- takeScreenshot: product_create_inventory + +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Save inventory through the editor toolbar" + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + label: "Wait for detail screen after Inventory" + +# ───────────────────────────────────────────────────────────────────── +# 5. Shipping — add weight and dimensions to the physical product +# ───────────────────────────────────────────────────────────────────── +- scrollUntilVisible: + element: + id: "productDetail_addMoreButton" + direction: DOWN + timeout: 10000 + label: "Scroll to Add more details for Shipping" + +- tapOn: + id: "productDetail_addMoreButton" + retryTapIfNoChange: true + label: "Open Add more details for Shipping" + +- extendedWaitUntil: + visible: "Shipping" + timeout: 10000 + label: "Require Shipping option" + +- tapOn: + text: "Shipping" + retryTapIfNoChange: true + label: "Open Shipping settings" + +- extendedWaitUntil: + visible: + id: "product_weight" + timeout: 15000 + label: "Wait for Shipping editor" + +- tapOn: { id: "product_weight", label: "Focus weight" } +- eraseText +- inputText: "1.25" +- tapOn: { id: "product_length", label: "Focus length" } +- eraseText +- inputText: "2.5" +- tapOn: { id: "product_width", label: "Focus width" } +- eraseText +- inputText: "3.75" +- tapOn: { id: "product_height", label: "Focus height" } +- eraseText +- inputText: "4.5" +- hideKeyboard + +- takeScreenshot: product_create_shipping + +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Save shipping through the editor toolbar" + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + label: "Wait for detail screen after Shipping" + +- scrollUntilVisible: + element: "Shipping" + direction: DOWN + timeout: 10000 + label: "Shipping row was added" + +# The selected creation type must still be a non-virtual Simple product before +# adding a downloadable file changes the summary to Downloadable product. +- scrollUntilVisible: + element: "Physical product" + direction: DOWN + timeout: 10000 + label: "Require the Simple physical product type" + +- assertVisible: + text: "Physical product" + label: "Draft is a Simple physical product" + +# Tags — create a run-unique tag and attach it to this run-owned draft. +- scrollUntilVisible: + element: + id: "productDetail_addMoreButton" + direction: DOWN + timeout: 10000 + label: "Reach Add more details for Tags" + +- tapOn: + id: "productDetail_addMoreButton" + retryTapIfNoChange: true + label: "Open Add more details for Tags" + +- scrollUntilVisible: + element: "Tags" + direction: DOWN + timeout: 10000 + label: "Require Tags option" + +- tapOn: + text: "Tags" + retryTapIfNoChange: true + label: "Open product tags" + +- extendedWaitUntil: + visible: + id: "productTagsRecycler" + timeout: 15000 + +- tapOn: + id: "addTagsEditText" +- inputText: "Maestro tag ${SUITE_RUN_ID}" +- pressKey: Enter +- hideKeyboard + +- extendedWaitUntil: + visible: + text: "Maestro tag ${SUITE_RUN_ID}" + childOf: + id: "selectedTagsGroup" + timeout: 15000 + label: "Run-owned tag is selected" + +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Save tags through the editor toolbar" + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 30000 + label: "Return after creating the run-owned tag" + +# Reopen Tags after the asynchronous create request has completed. If the +# first request returned no tag, repeat the same unique selection once. +- scrollUntilVisible: + element: + id: "productDetail_addMoreButton" + direction: DOWN + timeout: 10000 + +- runFlow: + when: + visible: "Tags" + commands: + - tapOn: + text: "Tags" + retryTapIfNoChange: true + label: "Reopen attached run-owned tag" + - extendedWaitUntil: + visible: + id: "productTagsRecycler" + timeout: 15000 + +- runFlow: + when: + notVisible: + id: "productTagsRecycler" + commands: + - tapOn: + id: "productDetail_addMoreButton" + retryTapIfNoChange: true + label: "Reopen Add more details for tag recovery" + - extendedWaitUntil: + visible: "Tags" + timeout: 10000 + - tapOn: + text: "Tags" + retryTapIfNoChange: true + label: "Reopen Tags for tag recovery" + +- extendedWaitUntil: + visible: + id: "productTagsRecycler" + timeout: 15000 + +- runFlow: + when: + notVisible: + text: "Maestro tag ${SUITE_RUN_ID}" + childOf: + id: "selectedTagsGroup" + commands: + - tapOn: + id: "addTagsEditText" + - inputText: "Maestro tag ${SUITE_RUN_ID}" + - pressKey: Enter + - hideKeyboard + - extendedWaitUntil: + visible: + text: "Maestro tag ${SUITE_RUN_ID}" + childOf: + id: "selectedTagsGroup" + timeout: 15000 + +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Confirm the run-owned tag after asynchronous creation" + +- extendedWaitUntil: + visible: "Maestro tag ${SUITE_RUN_ID}" + timeout: 30000 + label: "Run-owned tag is attached to the product draft" + +# ───────────────────────────────────────────────────────────────────── +# 6. Linked products — add one existing product as an Upsell +# ───────────────────────────────────────────────────────────────────── +- scrollUntilVisible: + element: + id: "productDetail_addMoreButton" + direction: DOWN + timeout: 10000 + label: "Scroll to Add more details for Linked products" + +- tapOn: + id: "productDetail_addMoreButton" + retryTapIfNoChange: true + label: "Open Add more details for Linked products" + +- scrollUntilVisible: + element: "Linked products" + direction: DOWN + timeout: 10000 + label: "Require Linked products option" + +- tapOn: + text: "Linked products" + retryTapIfNoChange: true + label: "Open Linked products" + +- extendedWaitUntil: + visible: + id: "add_upsell_products" + timeout: 15000 + label: "Wait for Linked products editor" + +- tapOn: + id: "add_upsell_products" + retryTapIfNoChange: true + label: "Open Upsell products" + +- extendedWaitUntil: + visible: "Add product" + timeout: 15000 + label: "Wait for empty Upsell list" + +- tapOn: + text: "Add product" + retryTapIfNoChange: true + label: "Choose an Upsell product" + +- extendedWaitUntil: + visible: + id: "productName" + timeout: 20000 + label: "Wait for product selection list" + +- tapOn: + id: "productName" + index: 0 + label: "Select first existing product as Upsell" + +- extendedWaitUntil: + visible: + id: "menu_done" + timeout: 10000 + label: "Wait for product selection action" + +- tapOn: + id: "menu_done" + label: "Confirm Upsell product" + +- extendedWaitUntil: + visible: + id: "productName" + timeout: 15000 + label: "Upsell product appears in the linked list" + +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Save selected Upsell through the toolbar" + +- extendedWaitUntil: + visible: + id: "upsells_count" + timeout: 10000 + label: "Wait for Upsell count" + +- assertVisible: + id: "upsells_count" + label: "One Upsell is attached to the product draft" + +- takeScreenshot: product_create_linked_product + +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Save Linked products through the toolbar" + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + label: "Wait for detail screen after Linked products" + +# ───────────────────────────────────────────────────────────────────── +# 7. Downloadable files — attach a deterministic URL and file name +# ───────────────────────────────────────────────────────────────────── +- scrollUntilVisible: + element: + id: "productDetail_addMoreButton" + direction: DOWN + timeout: 10000 + label: "Scroll to Add more details for Downloadable files" + +- tapOn: + id: "productDetail_addMoreButton" + retryTapIfNoChange: true + label: "Open Add more details for Downloadable files" + +- scrollUntilVisible: + element: "Downloadable files" + direction: DOWN + timeout: 10000 + label: "Require Downloadable files option" + +- tapOn: + text: "Downloadable files" + retryTapIfNoChange: true + label: "Open downloadable file sources" + +- extendedWaitUntil: + visible: + id: "add_downloadable_manually" + timeout: 10000 + label: "Wait for downloadable file source sheet" + +- tapOn: + id: "add_downloadable_manually" + label: "Enter downloadable file URL" + +- extendedWaitUntil: + visible: + id: "product_download_url" + timeout: 15000 + label: "Wait for downloadable file editor" + +- tapOn: { id: "product_download_url", label: "Focus file URL" } +- inputText: "https://example.com/maestro-${SUITE_RUN_ID}.pdf" +- tapOn: { id: "product_download_name", label: "Focus file name" } +- inputText: "Maestro download ${SUITE_RUN_ID}" +- hideKeyboard + +- extendedWaitUntil: + visible: + id: "menu_done" + timeout: 10000 + label: "Wait for Add downloadable file action" + +- tapOn: + id: "menu_done" + label: "Add downloadable file" + +- scrollUntilVisible: + element: "Downloadable files" + direction: DOWN + timeout: 15000 + label: "Downloadable files row was added" + +- assertVisible: "1 file" +- takeScreenshot: product_create_downloadable_file + +# ───────────────────────────────────────────────────────────────────── +# 8. Open the "Add more details" bottom sheet to unlock the Short +# description + Categories rows. The sheet's list items are built +# by ProductDetailBottomSheetBuilder; their visible text comes +# from the same string resources used on the detail rows. +# ───────────────────────────────────────────────────────────────────── +- scrollUntilVisible: + element: + id: "productDetail_addMoreButton" + direction: DOWN + timeout: 10000 + label: "Scroll to Add more details button" + +- tapOn: + id: "productDetail_addMoreButton" + retryTapIfNoChange: true + label: "Tap Add more details" + +- extendedWaitUntil: + visible: "Short description" + timeout: 10000 + label: "Wait for Add more details bottom sheet" + +- takeScreenshot: product_create_add_more_sheet_1 + +# ───────────────────────────────────────────────────────────────────── +# 5. Short description — Aztec visual editor +# ───────────────────────────────────────────────────────────────────── +- tapOn: + text: "Short description" + retryTapIfNoChange: true + label: "Tap Short description in sheet" + +- extendedWaitUntil: + visible: + id: "visualEditor" + timeout: 15000 + label: "Wait for Aztec visual editor" + +- tapOn: + id: "visualEditor" + label: "Focus the Aztec editor" + +# Maestro inputText does not support Unicode (see mobile-dev-inc/maestro#146), +# so we stick to plain ASCII — no em-dashes, smart quotes, etc. +- inputText: "Maestro short description ${SUITE_RUN_ID}" + +- hideKeyboard + +- takeScreenshot: product_create_short_description_entered + +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Save short description through the editor toolbar" + +- extendedWaitUntil: + visible: ".*PUBLISH.*|.*SAVE.*" + timeout: 10000 + label: "Wait for detail screen after description edit" + +- assertVisible: + text: "Maestro short description ${SUITE_RUN_ID}" + label: "Short description is attached to the draft" + +# ───────────────────────────────────────────────────────────────────── +# 6. Categories — open "Add more details" sheet again, pick Categories +# ───────────────────────────────────────────────────────────────────── +- scrollUntilVisible: + element: + id: "productDetail_addMoreButton" + direction: DOWN + timeout: 10000 + label: "Scroll to Add more details button again" + +- tapOn: + id: "productDetail_addMoreButton" + retryTapIfNoChange: true + label: "Tap Add more details (Categories)" + +- extendedWaitUntil: + visible: "Categories" + timeout: 10000 + label: "Wait for Add more details bottom sheet" + +- takeScreenshot: product_create_add_more_sheet_2 + +- tapOn: + text: "Categories" + retryTapIfNoChange: true + label: "Tap Categories in sheet" + +# ViewProductCategories navigates to ProductCategoriesFragment — +# a View-based screen (NOT the Compose ProductCategorySelectorScreen +# that's also in the codebase). Signal that the screen has loaded by +# waiting for its RecyclerView instead of a title string, since +# "Categories" text is ambiguous with the still-visible detail row. +- extendedWaitUntil: + visible: + id: "productCategoriesRecycler" + timeout: 15000 + label: "Wait for category list" + +- takeScreenshot: product_create_category_screen + +# Only the checkbox itself toggles selection — +# ProductCategoriesAdapter wires the click listener exclusively on +# `categoryCheckbox`, NOT on the row or the category name TextView +# (the row's `selectableItemBackground` is purely decorative). We +# therefore tap the first `categoryCheckbox` directly (index 0 = +# whichever category renders at the top of the list; which one is +# irrelevant for this smoke — the point is "product gets a category"). +- tapOn: + id: "categoryCheckbox" + index: 0 + retryTapIfNoChange: true + label: "Tick first category checkbox" + +# Small settle so the checked-state animation completes before the +# back press tears the fragment down. +- extendedWaitUntil: + visible: + id: "categoryCheckbox" + timeout: 3000 + +# ProductCategoriesFragment saves via viewModel.onBackButtonClicked +# → setResult back to the detail fragment when the user presses back. +- back + +- extendedWaitUntil: + visible: ".*PUBLISH.*|.*SAVE.*" + timeout: 15000 + label: "Wait for detail screen after category edit" + +- takeScreenshot: product_create_all_fields_set + +# ───────────────────────────────────────────────────────────────────── +# 7. Publish +# ───────────────────────────────────────────────────────────────────── +- extendedWaitUntil: + visible: ".*PUBLISH.*" + timeout: 10000 + label: "Wait for Publish action" + +- tapOn: + text: "PUBLISH" + retryTapIfNoChange: true + label: "Tap Publish" + +# After publishing the app may stay on the detail screen (with +# action label swapping PUBLISH → SAVE) or return to the products +# list. Either settling signal is good. +- extendedWaitUntil: + visible: ".*productsRecycler.*|.*SAVE.*|.*updated.*|.*published.*" + timeout: 45000 + label: "Wait for publish to complete" + +- takeScreenshot: product_create_published + +# ───────────────────────────────────────────────────────────────────── +# 8. Confirm the product is in the list +# ───────────────────────────────────────────────────────────────────── +- runFlow: + when: + notVisible: + id: "productsRecycler" + commands: + - back + +- extendedWaitUntil: + visible: + id: "productsRecycler" + timeout: 15000 + label: "Wait for products list" + +- tapOn: + id: "menu_search" + label: "Open product search" + +- extendedWaitUntil: + visible: + id: "search_src_text" + timeout: 10000 + +- tapOn: + id: "search_src_text" + +- inputText: ${SUITE_RUN_ID} + +- hideKeyboard + +# The search is debounced, and the staging backend can take a couple +# of seconds to return results for a just-published product. Generous +# timeout to accommodate that round-trip. Matching on the run id makes +# this assert immune to leftovers from previous runs. +- extendedWaitUntil: + visible: ".*Maestro Smoke Product ${SUITE_RUN_ID}.*" + timeout: 30000 + label: "Wait for the new product in search results" + +- assertVisible: ".*Maestro Smoke Product ${SUITE_RUN_ID}.*" + +- takeScreenshot: product_create_found_in_list + +# Reopen the exact run-owned product after publishing and verify the four +# edited detail surfaces persisted through the product update. +- tapOn: + text: ".*Maestro Smoke Product ${SUITE_RUN_ID}.*" + retryTapIfNoChange: true + label: "Reopen the published run-owned product" + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 20000 + label: "Wait for published product detail" + +# Main-description persistence in its editor. +- tapOn: + text: "Description" + retryTapIfNoChange: true + label: "Reopen persisted main description" +- extendedWaitUntil: + visible: + id: "visualEditor" + timeout: 15000 +- copyTextFrom: + id: "visualEditor" +- evalScript: | + if (!maestro.copiedText || !maestro.copiedText.includes('Maestro description ' + String(SUITE_RUN_ID))) { + throw new Error('Published product did not retain its main description'); + } +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Return from persisted main description" +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + +# Price persistence in its editor. +- scrollUntilVisible: + element: "Price" + direction: DOWN + timeout: 10000 +- tapOn: + text: "Price" + retryTapIfNoChange: true + label: "Reopen persisted price" +- extendedWaitUntil: + visible: + id: "product_regular_price" + timeout: 15000 +- copyTextFrom: + id: "product_regular_price" +- evalScript: | + if (!maestro.copiedText || maestro.copiedText.trim().replace(',', '.') !== '19.99') { + throw new Error('Published product did not retain regular price 19.99'); + } +- back +- extendedWaitUntil: + visible: "Price" + timeout: 10000 + +# Inventory persistence. +- scrollUntilVisible: + element: "Inventory" + direction: DOWN + timeout: 10000 +- tapOn: + text: "Inventory" + retryTapIfNoChange: true +- extendedWaitUntil: + visible: + id: "product_sku" + timeout: 15000 + +- copyTextFrom: + id: "product_sku" +- evalScript: | + if (!maestro.copiedText || !maestro.copiedText.includes('SMOKE-' + String(SUITE_RUN_ID))) { + throw new Error('Published product did not retain its run-owned SKU'); + } +- assertVisible: + id: "manageStock_switch" + checked: true + label: "Published product retains managed stock" +- copyTextFrom: + id: "product_stock_quantity" +- evalScript: | + if (!maestro.copiedText || !maestro.copiedText.includes('7')) { + throw new Error('Published product did not retain stock quantity 7'); + } +- takeScreenshot: product_persisted_inventory +- back + +- extendedWaitUntil: + visible: "Inventory" + timeout: 10000 + +# Shipping persistence. +- scrollUntilVisible: + element: "Shipping" + direction: DOWN + timeout: 10000 +- tapOn: + text: "Shipping" + retryTapIfNoChange: true +- extendedWaitUntil: + visible: + id: "product_weight" + timeout: 15000 + +- copyTextFrom: { id: "product_weight" } +- evalScript: | + if (!maestro.copiedText || !maestro.copiedText.includes('1.25')) { + throw new Error('Published product did not retain weight 1.25'); + } +- copyTextFrom: { id: "product_length" } +- evalScript: | + if (!maestro.copiedText || !maestro.copiedText.includes('2.5')) { + throw new Error('Published product did not retain length 2.5'); + } +- copyTextFrom: { id: "product_width" } +- evalScript: | + if (!maestro.copiedText || !maestro.copiedText.includes('3.75')) { + throw new Error('Published product did not retain width 3.75'); + } +- copyTextFrom: { id: "product_height" } +- evalScript: | + if (!maestro.copiedText || !maestro.copiedText.includes('4.5')) { + throw new Error('Published product did not retain height 4.5'); + } +- takeScreenshot: product_persisted_shipping +- back + +- extendedWaitUntil: + visible: "Shipping" + timeout: 10000 + +# Linked-products persistence. +- scrollUntilVisible: + element: "Linked products" + direction: DOWN + timeout: 10000 +- tapOn: + text: "Linked products" + retryTapIfNoChange: true +- extendedWaitUntil: + visible: + id: "upsells_count" + timeout: 15000 +- copyTextFrom: + id: "upsells_count" +- evalScript: | + if (!maestro.copiedText || !maestro.copiedText.trim().startsWith('1')) { + throw new Error('Published product did not retain its Upsell'); + } +- takeScreenshot: product_persisted_linked_product +- back + +- extendedWaitUntil: + visible: "Linked products" + timeout: 10000 + +# Downloadable-file persistence. +- scrollUntilVisible: + element: "Downloadable files" + direction: DOWN + timeout: 10000 +- tapOn: + text: "Downloadable files" + retryTapIfNoChange: true +- extendedWaitUntil: + visible: + id: "productDownloadsRecycler" + timeout: 15000 +- assertVisible: + text: "Maestro download ${SUITE_RUN_ID}" + label: "Published product retains downloadable file name" +- assertVisible: + text: "https://example.com/maestro-${SUITE_RUN_ID}.pdf" + label: "Published product retains downloadable file URL" +- takeScreenshot: product_persisted_downloadable_file +- back + +- extendedWaitUntil: + visible: "Downloadable files" + timeout: 10000 + +# Product-type persistence. A Simple physical product with an attached file is +# summarized as Downloadable product after publishing; Shipping persistence +# above proves it was not converted to a virtual product. +- scrollUntilVisible: + element: "Downloadable product" + direction: DOWN + timeout: 10000 + label: "Published product retains its Simple downloadable type" +- assertVisible: + text: "Downloadable product" + +# Tag persistence in the tags editor. +- scrollUntilVisible: + element: "Tags" + direction: UP + timeout: 10000 +- tapOn: + text: "Tags" + retryTapIfNoChange: true + label: "Reopen persisted tags" +- extendedWaitUntil: + visible: + id: "productTagsRecycler" + timeout: 15000 +- assertVisible: + text: "Maestro tag ${SUITE_RUN_ID}" + childOf: + id: "selectedTagsGroup" + label: "Published product retains the run-owned tag" +- back +- extendedWaitUntil: + visible: "Tags" + timeout: 10000 + +# Short-description persistence in its editor. +- scrollUntilVisible: + element: "Short description" + direction: DOWN + timeout: 10000 +- tapOn: + text: "Short description" + retryTapIfNoChange: true + label: "Reopen persisted short description" +- extendedWaitUntil: + visible: + id: "visualEditor" + timeout: 15000 +- copyTextFrom: + id: "visualEditor" +- evalScript: | + if (!maestro.copiedText || !maestro.copiedText.includes('Maestro short description ' + String(SUITE_RUN_ID))) { + throw new Error('Published product did not retain its short description'); + } +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Return from verified short description" +- extendedWaitUntil: + visible: "Linked products" + timeout: 10000 + +- back + +- extendedWaitUntil: + visible: + id: "productsRecycler" + timeout: 10000 + label: "Return to filtered product results" + +# Close search to leave the UI clean for downstream flows. +- back + +- extendedWaitUntil: + visible: + id: "productsRecycler" + timeout: 10000 diff --git a/.maestro/flows/products_detail.yaml b/.maestro/flows/products_detail.yaml new file mode 100644 index 000000000000..f8cacae6e9b2 --- /dev/null +++ b/.maestro/flows/products_detail.yaml @@ -0,0 +1,138 @@ +# Smoke Test: Products - Product Detail +# p2: products.detail.categories +# P2 ref: Products > Product details (all details) +# +# IMPORTANT: this flow opens "the first product" in the staging list. +# WooCommerce supports several product types (simple, variable, +# grouped, external, booking, subscription, …) and each type renders +# a DIFFERENT set of ComplexProperty rows on the detail screen: +# +# - Simple/physical → Price + Inventory + Product type + Shipping + … +# - Variable → Variations + Attributes + Product type +# - Grouped / External / Booking → no "Price" row (pricing handled elsewhere); +# no "Inventory" row on grouped/booking +# +# We therefore avoid asserting on type-specific labels like "Price" or +# "Inventory" — those would fail as soon as the first product on the +# staging store is, say, a Booking product (as happened on this run: +# the first product was "Tourist Activity" → Booking). Instead we +# assert on rows that render for EVERY product type: +# +# - "Product type" (always rendered; value varies) +# - "Reviews" (always rendered; "no approved reviews" placeholder) +# - "Categories" (always rendered; "Uncategorized" if unset) +# +# Verifies: +# - Product detail screen loads +# - Product name field + "Describe your product" area render +# - Universal ComplexProperty rows render (Product type, Reviews, Categories) +# +# Mutable description and product-type persistence is verified against the +# run-owned product in products_create.yaml. +appId: com.woocommerce.android.dev +name: "Products - Product detail view" +tags: + - smoke_extended + - flaky_quarantine + - products +--- +# Reuse the logged-in session — login flows run first and leave the +# app authenticated. See subflows/ensure_logged_in.yaml. +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +# Navigate to Products +- runFlow: + file: ../subflows/navigate_to_products.yaml + +# Open the first product +- tapOn: + id: "productName" + index: 0 + label: "Tap first product" + +# Wait for product detail screen. The "Describe your product" hint + +# "Write with AI" button render on every product type, so they're a +# safer readiness signal than price-related text. +- extendedWaitUntil: + visible: ".*GOT IT.*|.*Describe your product.*|.*Write with AI.*|.*PUBLISH.*" + timeout: 25000 + label: "Wait for product detail to load" + +# Dismiss "Write with AI" tooltip if it appears +- runFlow: + when: + visible: "GOT IT" + commands: + - tapOn: "GOT IT" + +# Wait for the product-name field (universal). +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + +- takeScreenshot: product_detail_top + +# Scroll to the properties RecyclerView so type-agnostic rows come +# into view. +- scrollUntilVisible: + element: + id: "propertiesRecyclerView" + direction: DOWN + timeout: 10000 + label: "Scroll to product properties" + +- takeScreenshot: product_detail_properties + +# Universal row 1: Product type (every WooCommerce product type +# renders this — simple / variable / grouped / external / booking / …). +- scrollUntilVisible: + element: "Product type" + direction: DOWN + timeout: 15000 + label: "Scroll to Product type row" + +- assertVisible: "Product type" + +- takeScreenshot: product_detail_type + +# Universal row 2: Reviews (renders with "no approved reviews" +# placeholder when the product has none). +- scrollUntilVisible: + element: "Reviews" + direction: DOWN + timeout: 15000 + label: "Scroll to Reviews row" + +- assertVisible: + text: "Reviews" + +# Universal row 3: Categories (falls back to "Uncategorized" when +# unset). +- scrollUntilVisible: + element: "Categories" + direction: DOWN + timeout: 15000 + label: "Scroll to Categories row" + +- assertVisible: + text: "Categories" + +# Type-specific rows. This provisional flow should target seeded/queryable +# products before promotion so these assertions are not store-order dependent. +- scrollUntilVisible: + element: ".*Price.*|.*Inventory.*|.*Shipping.*|.*Short description.*" + direction: DOWN + timeout: 10000 + label: "Scroll to Price / Inventory / Shipping / Short description" + +- takeScreenshot: product_detail_bottom + +# Go back to products list +- back + +- extendedWaitUntil: + visible: + id: "productsRecycler" + timeout: 10000 diff --git a/.maestro/flows/products_list_and_sort.yaml b/.maestro/flows/products_list_and_sort.yaml new file mode 100644 index 000000000000..75823388e100 --- /dev/null +++ b/.maestro/flows/products_list_and_sort.yaml @@ -0,0 +1,146 @@ +# Smoke Test: Products - List and Sort +# p2: products.list, products.sort, products.search +# P2 ref: Products > Product list, Sort product list +# +# Verifies: +# - Products list loads +# - Product cards show name, stock status, price, SKU +# - Sorting produces different first products for A-to-Z and Z-to-A +# - Search for products +# - Product filters +appId: com.woocommerce.android.dev +name: "Products - List, sort, and search" +tags: + - smoke_core + - products +--- +# Reuse the logged-in session — login flows run first and leave the +# app authenticated. See subflows/ensure_logged_in.yaml. +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +# Navigate to Products +- runFlow: + file: ../subflows/navigate_to_products.yaml + +# Verify products list loaded +- assertVisible: + id: "productsRecycler" + label: "Products list is visible" + +- assertVisible: + id: "productName" + label: "At least one product name is visible" + +# Validate first product has a non-empty name via JavaScript +- copyTextFrom: + id: "productName" + index: 0 +- evalScript: | + var name = maestro.copiedText; + if (!name || name.trim() === '') { + throw new Error('Product name is empty — list may not have loaded properly'); + } +- evalScript: ${output.productSearchText = maestro.copiedText.trim()} + +- takeScreenshot: products_list + +# Test sorting with two explicit, opposing choices. Requiring different first +# products proves the list was reordered instead of only opening the sheet. +- assertVisible: + id: "btn_product_sorting" + label: "Require product sorting control" +- tapOn: + id: "btn_product_sorting" + label: "Open product sorting" +- extendedWaitUntil: + visible: "Title: A to Z" + timeout: 10000 +- takeScreenshot: products_sort_options +- tapOn: + text: "Title: A to Z" + label: "Sort products A to Z" +- extendedWaitUntil: + visible: "A to Z" + timeout: 15000 +- extendedWaitUntil: + visible: + id: "productName" + timeout: 15000 +- copyTextFrom: + id: "productName" + index: 0 +- evalScript: ${output.firstProductAscending = maestro.copiedText.trim()} + +- tapOn: + id: "btn_product_sorting" + label: "Reopen product sorting" +- extendedWaitUntil: + visible: "Title: Z to A" + timeout: 10000 +- tapOn: + text: "Title: Z to A" + label: "Sort products Z to A" +- extendedWaitUntil: + visible: "Z to A" + timeout: 15000 +- extendedWaitUntil: + visible: + id: "productName" + timeout: 15000 +- copyTextFrom: + id: "productName" + index: 0 +- evalScript: | + var descending = maestro.copiedText.trim(); + if (!descending || descending === output.firstProductAscending) { + throw new Error('Opposing product sorts returned the same first product'); + } + +- takeScreenshot: products_sorted + +# Test search against the first visible product from the loaded list. +- tapOn: + id: "menu_search" + label: "Open product search" + +- extendedWaitUntil: + visible: + id: "search_src_text" + timeout: 10000 + +- tapOn: + id: "search_src_text" +- inputText: ${output.productSearchText} +- pressKey: Enter + +# Wait for search results +- extendedWaitUntil: + visible: + id: "productInfoContainer" + timeout: 15000 + +- assertVisible: + text: "${output.productSearchText}" + label: "Captured product appears in search results" + +- takeScreenshot: products_search_results + +# Go back from search +- back +- back + +# Test filters +- runFlow: + when: + visible: + id: "btn_product_filter" + commands: + - tapOn: + id: "btn_product_filter" + label: "Open product filters" + - extendedWaitUntil: + visible: "Stock status" + timeout: 10000 + - takeScreenshot: products_filters + - back diff --git a/.maestro/flows/products_media_upload.yaml b/.maestro/flows/products_media_upload.yaml new file mode 100644 index 000000000000..2150a1b50234 --- /dev/null +++ b/.maestro/flows/products_media_upload.yaml @@ -0,0 +1,309 @@ +# Smoke Test: Products - Media upload via WordPress media library +# p2: products.detail.media +# P2 ref: Products > Media upload +# +# Exercises the full WP-media-library upload path end-to-end: +# +# Product detail +# └─ tap "Add image" +# (ProductDetailViewModel.onAddImageButtonClicked → +# ViewProductImageGallery with showChooser = true) +# ▼ +# ProductImagesFragment ("Photos" screen) +# └─ image-source dialog auto-opens +# └─ tap "WordPress media library" +# (mediaPickerHelper.showMediaPicker(WP_MEDIA_LIBRARY)) +# ▼ +# MediaPickerActivity (external org.wordpress:mediapicker lib) +# └─ tap first image_thumbnail in the recycler +# └─ tap mnu_confirm_selection ("Add N") in the toolbar +# └─ result returned to ProductImagesFragment +# ▼ +# ProductImagesFragment — gallery now shows the image +# └─ back → product detail (dirty — new image in draft) +# └─ tap Save / Publish → product detail updated on backend +# +# ⚠️ STORE MUTATION: unlike most smoke flows, this one DOES persist a +# change — the image is attached to the product it opens. The flow +# therefore targets the automation-owned product created earlier in the +# suite by products_create.yaml, located by the unique suite run id. +# +# Source strings (res/values/strings.xml): +# - image_source_wp_media_library → "WordPress media library" +# - product_add_tool_bar_menu_button_done → "Publish" +# +# External lib resource IDs (from +# org.wordpress:mediapicker:0.3.5 AAR): +# - recycler — media thumbnail list +# - image_thumbnail — per-tile ImageView +# - mnu_confirm_selection — "Add N" action-mode menu item +# - soft_ask_view — storage-permission prompt (guarded) +appId: com.woocommerce.android.dev +name: "Products - Media upload via WP media library" +tags: + - smoke_extended + - flaky_quarantine + - products + - destructive +--- +# Reuse the logged-in session. +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +# Navigate to Products +- runFlow: + file: ../subflows/navigate_to_products.yaml + +# products_variations_and_tags.yaml leaves the Product type filter +# active when it runs earlier in the suite. Clear active filters before +# searching for this run's simple product. +- runFlow: + when: + visible: "Filters.*[0-9]" + commands: + - tapOn: + id: "btn_product_filter" + retryTapIfNoChange: true + label: "Open active product filters" + - extendedWaitUntil: + visible: + id: "filterList" + timeout: 15000 + - tapOn: + id: "menu_clear" + retryTapIfNoChange: true + label: "Clear active product filters" + - tapOn: + id: "filterList_btnShowProducts" + retryTapIfNoChange: true + label: "Show products without filters" + - extendedWaitUntil: + visible: + id: "productsRecycler" + timeout: 15000 + +# Open this run's newly-created product — never an arbitrary first row. +# This flow persists an image on whatever product it opens, and on the +# shared store index-0 can be a manual tester's real product. +- tapOn: + id: "menu_search" + label: "Open product search" + +- extendedWaitUntil: + visible: + id: "search_src_text" + timeout: 10000 + +- tapOn: + id: "search_src_text" +- inputText: "Maestro Smoke Product ${SUITE_RUN_ID}" +- hideKeyboard + +- extendedWaitUntil: + visible: + text: "Maestro Smoke Product ${SUITE_RUN_ID}" + timeout: 20000 + label: "Wait for this run's product in search results" + +- hideKeyboard + +- tapOn: + id: "productName" + index: 0 + retryTapIfNoChange: true + label: "Open this run's product" + +- extendedWaitUntil: + visible: ".*GOT IT.*|.*Describe your product.*|.*Write with AI.*|.*PUBLISH.*|.*SAVE.*" + timeout: 25000 + label: "Wait for product detail to load" + +- runFlow: + when: + visible: "GOT IT" + commands: + - tapOn: "GOT IT" + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + +- takeScreenshot: media_upload_product_detail + +# Tap the add-image affordance. Newly-created products show the +# empty-gallery "Add a product image" row; products that already have +# media show the gallery add icon/container. All paths call +# ProductDetailViewModel.onAddImageButtonClicked which navigates to +# ProductImagesFragment with showChooser = true. +- runFlow: + when: + visible: "Add a product image" + commands: + - tapOn: + text: "Add a product image" + retryTapIfNoChange: true + label: "Tap empty-gallery add image row" + - extendedWaitUntil: + visible: "Select an upload method" + timeout: 10000 + +- runFlow: + when: + notVisible: "Select an upload method" + commands: + - tapOn: + id: "addImageIcon" + retryTapIfNoChange: true + label: "Tap add image icon (gallery)" + +- runFlow: + when: + notVisible: "Select an upload method" + commands: + - tapOn: + id: "addImageContainer" + retryTapIfNoChange: true + label: "Tap add image container (fallback)" + +- runFlow: + when: + notVisible: "Select an upload method" + commands: + - tapOn: + text: "Add image" + retryTapIfNoChange: true + label: "Tap by Add image content description" + +# Dialog should be showing now. +- extendedWaitUntil: + visible: "Select an upload method" + timeout: 15000 + label: "Wait for image source dialog" + +- takeScreenshot: media_upload_source_dialog + +# Pick the WP media library source. The Choose / Camera entries are +# probed incidentally by the existence-assertions in the original +# smoke flow; here we specifically exercise the network-backed path. +- tapOn: + text: "WordPress media library" + label: "Tap WordPress media library" + +# MediaPickerActivity should open with the thumbnails RecyclerView. +# On a freshly-installed app it occasionally shows the permission +# soft-ask first — we grant it if it appears. The APK installer uses +# `adb install -g` which usually pre-grants runtime permissions, but +# the soft-ask is an in-app affordance unrelated to runtime perms, +# so we still guard for it. +- runFlow: + when: + visible: + id: "soft_ask_view" + commands: + - tapOn: + text: ".*Allow access.*|.*Allow.*" + +- extendedWaitUntil: + visible: + id: "recycler" + timeout: 30000 + label: "Wait for WP Media Library to load" + +- takeScreenshot: media_upload_wp_library + +# Select the first image in the grid. +- tapOn: + id: "image_thumbnail" + index: 0 + retryTapIfNoChange: true + label: "Tap first WP Media Library item" + +- takeScreenshot: media_upload_item_selected + +# Confirm the selection via the action-mode menu item. The +# mediapicker library labels this button dynamically based on +# selection count ("Add 1", "Add 2", …) so we target the id +# directly — it's stable across counts. +- tapOn: + id: "mnu_confirm_selection" + retryTapIfNoChange: true + label: "Tap Add in toolbar" + +# Back on ProductImagesFragment. The gallery now shows the freshly- +# picked image. The thumbnail's id is `productImage` +# (image_gallery_item.xml) — asserting it's visible proves the +# image-add round-trip worked. Allow generous timeout because the +# image is streamed from Jetpack / wp.com. +- extendedWaitUntil: + visible: + id: "productImage" + timeout: 45000 + label: "Wait for image to appear in product gallery" + +- assertVisible: + id: "productImage" + label: "Image is displayed in ProductImagesFragment" + +- takeScreenshot: media_upload_image_in_gallery + +# Navigate back to the product detail through ProductImagesFragment's +# toolbar navigation. That path calls onNavigateBackButtonClicked() +# and returns ExitWithResult(images) to ProductDetailFragment. A raw +# device back can pop the destination without delivering that result +# on some app/back-stack states. +- tapOn: + point: "7%,6%" + retryTapIfNoChange: true + label: "Tap image gallery toolbar back" + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + label: "Wait for product detail to reappear" + +# Save the product so the image is persisted on the backend. On an +# existing product the action is labelled "SAVE"; on a brand-new +# product it would read "PUBLISH". Match either. +- extendedWaitUntil: + visible: ".*SAVE.*|.*PUBLISH.*" + timeout: 10000 + label: "Wait for Save/Publish action" + +- tapOn: + text: ".*SAVE.*|.*PUBLISH.*" + retryTapIfNoChange: true + label: "Tap Save/Publish to persist the image" + +# A few outcomes are possible depending on app version + network: +# 1. The button label flips back to the non-dirty state and we +# stay on the detail screen. +# 2. A success snackbar appears. +# 3. The detail screen closes automatically. +# We accept any of those signals as "save worked". +- extendedWaitUntil: + visible: ".*updated.*|.*saved.*|.*productsRecycler.*|.*Describe your product.*" + timeout: 30000 + label: "Wait for save confirmation" + +- takeScreenshot: media_upload_saved + +# Return to the product list for the next flow. +- runFlow: + when: + notVisible: + id: "productsRecycler" + commands: + - back + +- runFlow: + when: + visible: "Discard" + commands: + - tapOn: "Discard" + +- extendedWaitUntil: + visible: + id: "productsRecycler" + timeout: 15000 diff --git a/.maestro/flows/products_variations_and_tags.yaml b/.maestro/flows/products_variations_and_tags.yaml new file mode 100644 index 000000000000..db99b1191649 --- /dev/null +++ b/.maestro/flows/products_variations_and_tags.yaml @@ -0,0 +1,339 @@ +# Smoke Test: Products - Variations via Product Type filter +# p2: products.detail.variations, products.detail.variation-attributes +# P2 ref: Products > Product details — Variations, Variation detail +# +# Exercises the real Variable-product path end-to-end: +# +# Products list +# └─ tap Filters (btn_product_filter) +# └─ Filter list (filterList) — tap "Product type" +# └─ Filter options (filterOptionList) — tap "Variable" +# └─ tap "Show products" (filterOptionList_btnShowProducts) +# └─ back on Filter list +# └─ tap "Show products" (filterList_btnShowProducts) +# └─ Products list now filtered to Variable +# └─ find a product row reporting N variations +# └─ Product detail — assert "Variations" row +# └─ tap Variations → VariationListFragment +# └─ assert variationList recycler +# └─ tap first variation +# └─ Variation detail — assert cardsRecyclerView +# +# ⚠️ STORE PREREQUISITE: at least ONE Variable product MUST exist on +# the store this flow is pointed at. If none exist, the assertion on +# a positive variation count after the filter is applied will fail — that's +# intentional. The test doesn't know whether "no variable products" is +# a regression in the app or missing store data. +# +# Mutable tag persistence is verified against the run-owned product in +# products_create.yaml. +# +# Key selectors (from the app's XML layouts): +# - btn_product_filter — WCToggleOutlinedButton on the +# sort+filters card above the list +# - filterList — RecyclerView on the Filter list +# screen (fragment_product_filter_list) +# - filterList_btnShowProducts — "Show products" CTA +# - filterOptionList — RecyclerView on the per-filter +# options screen +# - filterOptionList_btnShowProducts — "Show products" CTA on options +# - empty_view — shown on products list when no +# results match the active filter +# - variationList — RecyclerView on VariationListFragment +# - cardsRecyclerView — RecyclerView on variation detail +appId: com.woocommerce.android.dev +name: "Products - Variations via Variable filter" +tags: + - smoke_extended + - flaky_quarantine + - products +--- +# Reuse the logged-in session. +- runFlow: + file: ../subflows/ensure_logged_in.yaml + +# Navigate to Products +- runFlow: + file: ../subflows/navigate_to_products.yaml + +# ───────────────────────────────────────────────────────────────────── +# 1. Open the product filter list +# ───────────────────────────────────────────────────────────────────── +- scrollUntilVisible: + element: + id: "btn_product_filter" + direction: UP + timeout: 10000 + label: "Make sure the Filters button is visible" + +- tapOn: + id: "btn_product_filter" + retryTapIfNoChange: true + label: "Tap Filters button" + +- extendedWaitUntil: + visible: + id: "filterList" + timeout: 15000 + label: "Wait for filter list screen" + +- takeScreenshot: products_filter_list + +# ───────────────────────────────────────────────────────────────────── +# 2. Tap "Product type" filter row → 3. Tap "Variable" +# ───────────────────────────────────────────────────────────────────── +- tapOn: + text: "Product type" + retryTapIfNoChange: true + label: "Tap Product type filter row" + +- extendedWaitUntil: + visible: + id: "filterOptionList" + timeout: 15000 + label: "Wait for Product type options" + +- takeScreenshot: products_filter_type_options + +- tapOn: + text: "Variable" + retryTapIfNoChange: true + label: "Select Variable" + +# ───────────────────────────────────────────────────────────────────── +# 4. Apply the filter. "Show products" on the options screen already +# pops all the way back to the products list on current app +# builds; on older builds it only returns to the filter list and +# requires a second "Show products" tap there. Handle both. +# ───────────────────────────────────────────────────────────────────── +- tapOn: + id: "filterOptionList_btnShowProducts" + retryTapIfNoChange: true + label: "Confirm Variable selection" + +# If the filter list is still visible, tap its own Show products. +- runFlow: + when: + visible: + id: "filterList_btnShowProducts" + commands: + - tapOn: + id: "filterList_btnShowProducts" + retryTapIfNoChange: true + label: "Apply filters and return to list" + +# ───────────────────────────────────────────────────────────────────── +# 5. Products list, now filtered. If the list is empty +# (empty_view rendered), fail with a clear message — the fix is +# a staging-data change, not a test change. +# ───────────────────────────────────────────────────────────────────── +# Wait until either the first product row OR the empty-state view +# has painted. Both are hosted inside productsRecycler's container, +# and only one renders at a time based on the filter result. +- extendedWaitUntil: + visible: + id: "productsRecycler" + timeout: 20000 + label: "Wait for products list container" + +- assertNotVisible: + id: "empty_view" + label: "FAIL: store has no Variable products" + +- assertVisible: + id: "productName" + label: "At least one Variable product is in the filtered list" + +- takeScreenshot: products_filtered_variable + +# A positive variation count is the app's own signal that this parent product +# has variations. This avoids coupling the flow to a named store fixture. +- scrollUntilVisible: + element: ".*[1-9][0-9]* variations?.*" + direction: DOWN + timeout: 20000 + label: "Find a Variable product with variations" + +# ───────────────────────────────────────────────────────────────────── +# 6. Open a variable product from the filtered list +# ───────────────────────────────────────────────────────────────────── +- tapOn: + text: ".*[1-9][0-9]* variations?.*" + retryTapIfNoChange: true + label: "Open discovered Variable product" + +- extendedWaitUntil: + visible: ".*GOT IT.*|.*Describe your product.*|.*Write with AI.*|.*PUBLISH.*|.*SAVE.*" + timeout: 25000 + label: "Wait for product detail" + +- runFlow: + when: + visible: "GOT IT" + commands: + - tapOn: "GOT IT" + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + +- takeScreenshot: products_variable_detail + +# ───────────────────────────────────────────────────────────────────── +# 7. Open Tags, then require the existing Variations path and a real +# variation detail. Products without tags expose Tags under Add more +# details; products with tags expose it directly on the detail screen. +# ───────────────────────────────────────────────────────────────────── +- scrollUntilVisible: + element: + id: "productDetail_addMoreButton" + direction: DOWN + timeout: 15000 + label: "Scroll to Add more details" + +- tapOn: + id: "productDetail_addMoreButton" + retryTapIfNoChange: true + label: "Open additional product details" + +- extendedWaitUntil: + visible: + id: "productDetailInfo_optionsList" + timeout: 10000 + +# An untagged product offers Tags in the additional-details sheet. +- runFlow: + when: + visible: "Tags" + commands: + - tapOn: + text: "Tags" + retryTapIfNoChange: true + label: "Open empty product tags" + - extendedWaitUntil: + visible: + id: "productTagsRecycler" + timeout: 15000 + +# A tagged product omits Tags from the sheet because the direct detail row +# already exists. Return to the product and open that row instead. +- runFlow: + when: + notVisible: + id: "productTagsRecycler" + commands: + - back + - scrollUntilVisible: + element: "Tags" + direction: DOWN + timeout: 15000 + label: "Find existing Tags row" + - tapOn: + text: "Tags" + retryTapIfNoChange: true + label: "Open existing product tags" + +- extendedWaitUntil: + visible: + id: "productTagsRecycler" + timeout: 15000 + label: "Wait for product tags" + +- takeScreenshot: products_variable_tags +- back + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + +- scrollUntilVisible: + element: "Variations" + direction: DOWN + timeout: 15000 + label: "Require existing Variations row" + +- scrollUntilVisible: + element: ".*Variations attributes.*|.*Attributes.*" + direction: DOWN + timeout: 15000 + label: "Require variation attributes" + +- takeScreenshot: products_variable_detail_rows + +- scrollUntilVisible: + element: "Variations" + direction: UP + timeout: 15000 + label: "Return to Variations row" + +- tapOn: + text: "Variations" + retryTapIfNoChange: true + label: "Open Variations" + +- extendedWaitUntil: + visible: + id: "variationList" + timeout: 20000 + label: "Wait for variations list" + +- tapOn: + id: "variationList" + index: 0 + retryTapIfNoChange: true + label: "Open first available variation" + +- extendedWaitUntil: + visible: + id: "cardsRecyclerView" + timeout: 20000 + label: "Wait for variation detail" + +- takeScreenshot: products_variation_detail +- back + +- extendedWaitUntil: + visible: + id: "variationList" + timeout: 10000 +- back + +- extendedWaitUntil: + visible: + id: "editText" + timeout: 10000 + +# ───────────────────────────────────────────────────────────────────── +# 8. Back out cleanly — product detail → products list +# ───────────────────────────────────────────────────────────────────── +- back + +# If a Discard changes dialog appears (shouldn't — we didn't edit), +# dismiss it. +- runFlow: + when: + visible: "Discard" + commands: + - tapOn: "Discard" + +- extendedWaitUntil: + visible: + id: "productsRecycler" + timeout: 15000 + +# If the product search is still active, exit it so downstream flows +# start from the normal products list. +- runFlow: + when: + visible: + id: "search_src_text" + commands: + - back + - back + +- extendedWaitUntil: + visible: + id: "productsRecycler" + timeout: 10000 diff --git a/.maestro/scripts/annotate-run.py b/.maestro/scripts/annotate-run.py new file mode 100644 index 000000000000..42d18e402cd7 --- /dev/null +++ b/.maestro/scripts/annotate-run.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Render a concise Buildkite-ready Markdown summary from Maestro artifacts.""" + +from __future__ import annotations + +import argparse +import json +import os +import xml.etree.ElementTree as ET +from pathlib import Path + + +def junit_totals(path: Path) -> tuple[int, int, int, list[str]]: + root = ET.parse(path).getroot() + suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite")) + tests = sum(int(suite.get("tests", len(suite.findall("testcase")))) for suite in suites) + failures = sum( + int(suite.get("failures", "0")) + int(suite.get("errors", "0")) for suite in suites + ) + skipped = sum(int(suite.get("skipped", "0")) for suite in suites) + failed_names = [ + case.get("name", "unnamed") + for case in root.iter("testcase") + if case.find("failure") is not None or case.find("error") is not None + ] + return tests, failures, skipped, failed_names + + +def render(junit: Path, summary: Path | None = None) -> str: + tests, failures, skipped, failed_names = junit_totals(junit) + status = "PASS" if failures == 0 else "FAIL" + if summary and summary.is_file(): + status = str(json.loads(summary.read_text(encoding="utf-8")).get("status", status)) + lines = [ + f"### Maestro smoke: {status}", + "", + f"{tests} tests · {failures} failures · {skipped} skipped", + ] + if failed_names: + lines.extend(["", "Failures:"]) + lines.extend(f"- `{name}`" for name in failed_names[:8]) + build_url = os.environ.get("BUILDKITE_BUILD_URL") + job_id = os.environ.get("BUILDKITE_JOB_ID") + if build_url and job_id: + lines.extend(["", f"[Open job artifacts]({build_url}#job-{job_id})"]) + return "\n".join(lines) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--junit", required=True, type=Path) + parser.add_argument("--summary", type=Path) + args = parser.parse_args() + print(render(args.junit, args.summary), end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.maestro/scripts/check-smoke-coverage.py b/.maestro/scripts/check-smoke-coverage.py new file mode 100755 index 000000000000..813c04cfcac6 --- /dev/null +++ b/.maestro/scripts/check-smoke-coverage.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Offline coverage check for Maestro smoke flows.""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + + +ID_RE = re.compile(r"^\s*-\s+id:\s*([A-Za-z0-9_.-]+)\s*$") +FLOW_RE = re.compile(r"^\s+flow:\s*(.+?)\s*$") +MANUAL_RE = re.compile(r"^\s+manual:\s*(.+?)\s*$") +P2_RE = re.compile(r"^#\s*p2:\s*(.+?)\s*$", re.IGNORECASE) +P2_PROBE_RE = re.compile(r"^#\s*p2\s+probe:\s*(.+?)\s*$", re.IGNORECASE) + + +def parse_header_ids(value: str) -> set[str]: + ids = re.split(r"\s+\(", value, maxsplit=1)[0] + return {item.strip() for item in ids.split(",") if item.strip()} + + +def parse_snapshot(path: Path) -> tuple[dict[str, dict[str, str]], set[str]]: + items: dict[str, dict[str, str]] = {} + duplicates: set[str] = set() + current: str | None = None + for line in path.read_text(encoding="utf-8").splitlines(): + id_match = ID_RE.match(line) + if id_match: + current = id_match.group(1) + if current in items: + duplicates.add(current) + items[current] = {} + continue + if current is None: + continue + flow_match = FLOW_RE.match(line) + if flow_match: + items[current]["flow"] = flow_match.group(1).strip().strip('"') + continue + manual_match = MANUAL_RE.match(line) + if manual_match: + items[current]["manual"] = manual_match.group(1).strip().strip('"') + continue + return items, duplicates + + +def parse_flow_headers(flows_dir: Path) -> tuple[dict[str, set[str]], dict[str, set[str]]]: + claims: dict[str, set[str]] = {} + probes: dict[str, set[str]] = {} + for flow in sorted(flows_dir.glob("*.yaml")): + flow_claims: set[str] = set() + flow_probes: set[str] = set() + for line in flow.read_text(encoding="utf-8").splitlines()[:40]: + probe_match = P2_PROBE_RE.match(line) + if probe_match: + flow_probes.update(parse_header_ids(probe_match.group(1))) + continue + claim_match = P2_RE.match(line) + if claim_match: + flow_claims.update(parse_header_ids(claim_match.group(1))) + claims[str(flow)] = flow_claims + probes[str(flow)] = flow_probes + return claims, probes + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--coverage", default=".maestro/smoke-coverage.yaml", type=Path) + parser.add_argument("--flows-dir", default=".maestro/flows", type=Path) + args = parser.parse_args() + + items, duplicates = parse_snapshot(args.coverage) + flow_claims, flow_probes = parse_flow_headers(args.flows_dir) + errors: list[str] = [] + + for item_id in sorted(duplicates): + errors.append(f"{args.coverage}: duplicate item id {item_id}") + + for item_id, data in sorted(items.items()): + has_flow = bool(data.get("flow")) + has_manual = bool(data.get("manual")) + if has_flow and has_manual: + errors.append(f"{args.coverage}: item {item_id} has both flow and manual reason") + elif not has_flow and not has_manual: + errors.append(f"{args.coverage}: item {item_id} has neither flow nor manual reason") + + known_ids = set(items) + for flow in flow_claims: + claims = flow_claims[flow] + probes = flow_probes[flow] + if not claims and not probes: + errors.append(f"{flow}: missing '# p2:' or '# P2 probe:' header") + for item_id in sorted((claims | probes) - known_ids): + errors.append(f"{flow}: unknown p2 id {item_id}") + + for item_id in sorted(claims & known_ids): + mapped_flow = items[item_id].get("flow", "") + if mapped_flow != flow: + errors.append(f"{flow}: p2 id {item_id} maps to {mapped_flow or 'manual coverage'}") + + for item_id in sorted(probes & known_ids): + if items[item_id].get("flow"): + errors.append(f"{flow}: probe id {item_id} is counted as automated coverage") + + for item_id, data in sorted(items.items()): + flow = data.get("flow", "") + if flow and item_id not in flow_claims.get(flow, set()): + errors.append(f"{args.coverage}: item {item_id} maps to {flow}, but that flow does not declare it") + + if errors: + print("\n".join(errors), file=sys.stderr) + return 1 + print(f"Coverage snapshot OK: {len(items)} items, {len(flow_claims)} flows") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.maestro/scripts/check-toolchain.py b/.maestro/scripts/check-toolchain.py new file mode 100755 index 000000000000..4d86ea81d10b --- /dev/null +++ b/.maestro/scripts/check-toolchain.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Report the local Maestro toolchain against the repository pin.""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + + +VERSION_FILE = Path(__file__).resolve().parents[1] / "toolchain.properties" +MAESTRO_VERSION_PATTERN = re.compile(r"(?m)^\s*(\d+\.\d+\.\d+(?:[-+][^\s]+)?)\s*$") +JAVA_VERSION_PATTERN = re.compile(r'(?:openjdk|java) version "([^"]+)"') + + +def load_versions() -> dict[str, str]: + return dict( + line.split("=", 1) + for line in VERSION_FILE.read_text(encoding="utf-8").splitlines() + if line.strip() + ) + + +def command_output(command: list[str]) -> str: + try: + result = subprocess.run(command, capture_output=True, text=True, check=True) + except FileNotFoundError: + print(f"Required command not found: {command[0]}", file=sys.stderr) + raise SystemExit(2) from None + except subprocess.CalledProcessError as error: + print( + f"Could not run {' '.join(command)} successfully (exit {error.returncode})", + file=sys.stderr, + ) + raise SystemExit(2) from None + return result.stdout + result.stderr + + +def parsed_version(pattern: re.Pattern[str], output: str, tool: str) -> str: + match = pattern.search(output) + if match is None: + print(f"Could not parse {tool} version output", file=sys.stderr) + raise SystemExit(2) + return match.group(1) + + +def main() -> int: + expected = load_versions() + maestro_output = command_output(["maestro", "--version"]) + java_output = command_output(["java", "-version"]) + maestro_version = parsed_version(MAESTRO_VERSION_PATTERN, maestro_output, "Maestro") + java_version = parsed_version(JAVA_VERSION_PATTERN, java_output, "Java") + + print(f"Maestro: expected {expected['maestro']}, actual {maestro_version}") + print(f"Java: expected major {expected['java']}, actual {java_version}") + if maestro_version != expected["maestro"]: + print( + f"Maestro version mismatch: expected {expected['maestro']}, actual {maestro_version}", + file=sys.stderr, + ) + return 1 + java_major = java_version.split(".", 1)[0] + if java_major != expected["java"]: + print( + f"Java version mismatch: expected major {expected['java']}, actual {java_version}", + file=sys.stderr, + ) + return 1 + print("Maestro toolchain OK") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.maestro/scripts/configure-toolchain.sh b/.maestro/scripts/configure-toolchain.sh new file mode 100644 index 000000000000..be4de8965940 --- /dev/null +++ b/.maestro/scripts/configure-toolchain.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# Source from CI wrappers to select Java 21 and install the verified Maestro pin. + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + echo "Source this script so JAVA_HOME and PATH remain active: source ${BASH_SOURCE[0]}" >&2 + exit 2 +fi + +TOOLCHAIN_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TOOLCHAIN_PROPERTIES="$TOOLCHAIN_SCRIPT_DIR/../toolchain.properties" +PINNED_MAESTRO="$(awk -F= '$1 == "maestro" { print $2 }' "$TOOLCHAIN_PROPERTIES")" +PINNED_MAESTRO_SHA256="$(awk -F= '$1 == "maestro_sha256" { print $2 }' "$TOOLCHAIN_PROPERTIES")" +PINNED_JAVA="$(awk -F= '$1 == "java" { print $2 }' "$TOOLCHAIN_PROPERTIES")" + +current_java_major() { + java -version 2>&1 | sed -nE 's/.*version "([0-9]+).*/\1/p' | head -n 1 +} + +if [[ "$(current_java_major)" != "$PINNED_JAVA" ]]; then + if [[ "$(uname)" == "Darwin" && -x /usr/libexec/java_home ]]; then + PINNED_JAVA_HOME="$(/usr/libexec/java_home -v "$PINNED_JAVA" 2>/dev/null || true)" + if [[ -n "$PINNED_JAVA_HOME" ]]; then + export JAVA_HOME="$PINNED_JAVA_HOME" + export PATH="$JAVA_HOME/bin:$PATH" + fi + fi +fi +if [[ "$(current_java_major)" != "$PINNED_JAVA" ]]; then + echo "CI requires Java $PINNED_JAVA, but no matching JDK is configured on this agent." >&2 + return 2 +fi + +CURRENT_MAESTRO="$(maestro --version 2>/dev/null | tail -n 1 || true)" +if [[ "$CURRENT_MAESTRO" != "$PINNED_MAESTRO" ]]; then + MAESTRO_TOOLCHAIN_DIR="${MAESTRO_TOOLCHAIN_ROOT:-${BUILDKITE_BUILD_CHECKOUT_PATH:-$PWD}/build/maestro-toolchain}" + MAESTRO_INSTALL_DIR="$MAESTRO_TOOLCHAIN_DIR/maestro-$PINNED_MAESTRO-$PINNED_MAESTRO_SHA256" + MAESTRO_PINNED_BIN="$MAESTRO_INSTALL_DIR/bin/maestro" + if [[ ! -x "$MAESTRO_PINNED_BIN" ]]; then + echo "Installing verified Maestro $PINNED_MAESTRO into the CI job workspace." + mkdir -p "$MAESTRO_TOOLCHAIN_DIR" + MAESTRO_ARCHIVE="$(mktemp "$MAESTRO_TOOLCHAIN_DIR/maestro.zip.XXXXXX")" + MAESTRO_STAGE="$(mktemp -d "$MAESTRO_TOOLCHAIN_DIR/install.XXXXXX")" + MAESTRO_RELEASE_URL="https://github.com/mobile-dev-inc/Maestro/releases/download/cli-$PINNED_MAESTRO/maestro.zip" + if ! curl -fsSL "$MAESTRO_RELEASE_URL" -o "$MAESTRO_ARCHIVE"; then + rm -f "$MAESTRO_ARCHIVE" + rm -rf "$MAESTRO_STAGE" + return 2 + fi + if command -v sha256sum >/dev/null 2>&1; then + MAESTRO_ARCHIVE_SHA256="$(sha256sum "$MAESTRO_ARCHIVE" | awk '{print $1}')" + elif command -v shasum >/dev/null 2>&1; then + MAESTRO_ARCHIVE_SHA256="$(shasum -a 256 "$MAESTRO_ARCHIVE" | awk '{print $1}')" + else + echo "Cannot verify Maestro: sha256sum or shasum is required." >&2 + rm -f "$MAESTRO_ARCHIVE" + rm -rf "$MAESTRO_STAGE" + return 2 + fi + if [[ "$MAESTRO_ARCHIVE_SHA256" != "$PINNED_MAESTRO_SHA256" ]]; then + echo "Maestro archive checksum mismatch: expected $PINNED_MAESTRO_SHA256, actual $MAESTRO_ARCHIVE_SHA256" >&2 + rm -f "$MAESTRO_ARCHIVE" + rm -rf "$MAESTRO_STAGE" + return 2 + fi + if ! unzip -q "$MAESTRO_ARCHIVE" -d "$MAESTRO_STAGE"; then + rm -f "$MAESTRO_ARCHIVE" + rm -rf "$MAESTRO_STAGE" + return 2 + fi + rm -f "$MAESTRO_ARCHIVE" + if [[ ! -x "$MAESTRO_STAGE/maestro/bin/maestro" ]]; then + echo "Verified Maestro archive does not contain maestro/bin/maestro." >&2 + rm -rf "$MAESTRO_STAGE" + return 2 + fi + if [[ -e "$MAESTRO_INSTALL_DIR" ]]; then + echo "Existing pinned Maestro directory is incomplete: $MAESTRO_INSTALL_DIR" >&2 + rm -rf "$MAESTRO_STAGE" + return 2 + fi + if ! mv "$MAESTRO_STAGE/maestro" "$MAESTRO_INSTALL_DIR"; then + rm -rf "$MAESTRO_STAGE" + return 2 + fi + rm -rf "$MAESTRO_STAGE" + fi + export PATH="$MAESTRO_INSTALL_DIR/bin:$PATH" + CURRENT_MAESTRO="$(maestro --version 2>/dev/null | tail -n 1 || true)" + if [[ "$CURRENT_MAESTRO" != "$PINNED_MAESTRO" ]]; then + echo "Verified Maestro archive reported unexpected version: $CURRENT_MAESTRO" >&2 + return 2 + fi +fi + +python3 "$TOOLCHAIN_SCRIPT_DIR/check-toolchain.py" diff --git a/.maestro/scripts/doctor.py b/.maestro/scripts/doctor.py new file mode 100755 index 000000000000..6b998bd5028e --- /dev/null +++ b/.maestro/scripts/doctor.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +"""Pre-flight doctor for WooCommerce Android Maestro smoke runs.""" + +from __future__ import annotations + +import argparse +import os +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +from smoke_plan import PROFILES, flow_tags, selected_flows + + +SCRIPT_DIR = Path(__file__).resolve().parent +REPO_ROOT = SCRIPT_DIR.parent.parent +DEFAULT_ENV_FILE = REPO_ROOT / ".maestro" / ".env.local" +LINT_ENV = SCRIPT_DIR / "lint-env.py" +SEED_SCRIPT = SCRIPT_DIR / "seed-fixtures.py" +CHECK_TOOLCHAIN = SCRIPT_DIR / "check-toolchain.py" +SHARED_STORE_HOST = "inpersonpayments.wpcomstaging.com" + +ASSIGNMENT_RE = re.compile(r"^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$") +REF_RE = re.compile(r"\$\{(WOO_[A-Z0-9_]+)\}") +SUBFLOW_LOGIN_RE = re.compile(r"subflows/(ensure_logged_in|login)\.yaml") + +@dataclass +class Check: + status: str + message: str + + +def parse_csv(value: str | None) -> list[str] | None: + if value is None: + return None + return [item.strip() for item in value.split(",") if item.strip()] + + +def parse_env_file(path: Path) -> dict[str, str]: + values: dict[str, str] = {} + if not path.exists(): + return values + for raw_line in path.read_text(errors="replace").splitlines(): + stripped = raw_line.strip() + if not stripped or stripped.startswith("#"): + continue + match = ASSIGNMENT_RE.match(stripped) + if not match: + continue + name, value = match.groups() + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: + value = value[1:-1] + values[name] = value + return values + + +def referenced_env(flows: list[Path], seed: bool) -> set[str]: + refs: set[str] = set() + for flow in flows: + text = flow.read_text(errors="replace") + flow_refs = set(REF_RE.findall(text)) + if flow.name == "login_not_woo_store.yaml": + flow_refs.difference_update( + {"WOO_NOT_A_WOO_STORE_WPCOM_EMAIL", "WOO_NOT_A_WOO_STORE_WPCOM_PASSWORD"} + ) + refs.update(flow_refs) + if SUBFLOW_LOGIN_RE.search(text): + refs.update({"WOO_JETPACK_STORE_URL", "WOO_WPCOM_EMAIL", "WOO_WPCOM_PASSWORD"}) + if seed: + refs.update({"WOO_STORE_URL", "WOO_CONSUMER_KEY", "WOO_CONSUMER_SECRET"}) + return refs + + +def candidates_for(ref: str, store: str) -> list[str]: + upper = store.upper() + mapped = { + "WOO_JETPACK_STORE_URL": [ + f"MAESTRO_WOO_{upper}_JETPACK_STORE_URL", + f"MAESTRO_WOO_{upper}_STORE_URL", + ], + "WOO_STORE_URL": [ + f"MAESTRO_WOO_{upper}_JETPACK_STORE_URL", + f"MAESTRO_WOO_{upper}_STORE_URL", + ], + "WOO_WPCOM_EMAIL": [ + f"MAESTRO_WOO_{upper}_WPCOM_EMAIL", + f"MAESTRO_WOO_{upper}_EMAIL", + ], + "WOO_WPCOM_PASSWORD": [ + f"MAESTRO_WOO_{upper}_WPCOM_PASSWORD", + f"MAESTRO_WOO_{upper}_PASSWORD", + ], + "WOO_CONSUMER_KEY": [ + f"MAESTRO_WOO_{upper}_CONSUMER_KEY", + ], + "WOO_CONSUMER_SECRET": [ + f"MAESTRO_WOO_{upper}_CONSUMER_SECRET", + ], + "WOO_NO_JETPACK_SITE_URL": ["MAESTRO_WOO_NO_JETPACK_SITE_URL", "MAESTRO_WOO_JN_SITE_URL"], + "WOO_NO_JETPACK_SITE_ADMIN_USERNAME": [ + "MAESTRO_WOO_NO_JETPACK_SITE_ADMIN_USERNAME", + "MAESTRO_WOO_JN_USERNAME", + ], + "WOO_NO_JETPACK_SITE_ADMIN_PASSWORD": [ + "MAESTRO_WOO_NO_JETPACK_SITE_ADMIN_PASSWORD", + "MAESTRO_WOO_JN_PASSWORD", + ], + } + return mapped.get(ref, [f"MAESTRO_{ref}"]) + + +def has_value(env: dict[str, str], names: list[str]) -> bool: + return any(bool(env.get(name, "")) for name in names) + + +def url_host(value: str) -> str: + value = value.removeprefix("http://").removeprefix("https://") + return value.split("/", 1)[0].split(":", 1)[0].lower() + + +def command_check(name: str) -> Check: + path = shutil.which(name) + if path: + return Check("ok", f"{name} found at {path}") + return Check("fail", f"{name} not found on PATH") + + +def adb_devices() -> list[str]: + if not shutil.which("adb"): + return [] + result = subprocess.run(["adb", "devices"], capture_output=True, text=True) + devices: list[str] = [] + for line in result.stdout.splitlines()[1:]: + parts = line.split() + if len(parts) >= 2 and parts[1] == "device": + devices.append(parts[0]) + return devices + + +def toolchain_check() -> Check: + result = subprocess.run( + [sys.executable, str(CHECK_TOOLCHAIN)], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + return Check("ok", "Maestro toolchain matches the repository pin") + details = [line.strip() for line in result.stderr.splitlines() if line.strip()] + message = details[-1] if details else "Maestro toolchain check failed" + return Check("fail", message) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Validate Maestro smoke-test prerequisites without running flows.") + parser.add_argument("--profile", choices=sorted(PROFILES), default="core") + parser.add_argument("--store", choices=("lab", "shared")) + parser.add_argument("--include-tags") + parser.add_argument("--exclude-tags") + parser.add_argument("--include-quarantine", action="store_true") + parser.add_argument("--device", help="Expected adb serial. AVD-name matching is handled by the runner.") + parser.add_argument("--env-file", type=Path, default=DEFAULT_ENV_FILE) + parser.add_argument("--seed", action="store_true") + args = parser.parse_args() + + profile = PROFILES[args.profile] + store = args.store or profile.store + include_tags = parse_csv(args.include_tags) + if include_tags is None: + include_tags = list(profile.include) + exclude_tags = parse_csv(args.exclude_tags) + if exclude_tags is None: + exclude_tags = list(profile.exclude) + if args.include_quarantine: + exclude_tags = [tag for tag in exclude_tags if tag != "flaky_quarantine"] + + checks: list[Check] = [ + command_check("bash"), + command_check("python3"), + command_check("maestro"), + command_check("adb"), + toolchain_check(), + ] + + if args.env_file.exists(): + lint_command = [str(LINT_ENV), "--file", str(args.env_file)] + if args.seed: + lint_command.append("--seed") + lint = subprocess.run(lint_command, cwd=REPO_ROOT, capture_output=True, text=True) + checks.append(Check("ok" if lint.returncode == 0 else "fail", f"{args.env_file} lint {'passed' if lint.returncode == 0 else 'failed'}")) + else: + checks.append(Check("warn", f"{args.env_file} not found; expecting credentials from exported environment or CI secrets")) + + env = dict(os.environ) + env.update(parse_env_file(args.env_file)) + + flows = selected_flows(include_tags, exclude_tags) + checks.append(Check("ok" if flows else "fail", f"{len(flows)} flow(s) selected for profile {args.profile}")) + + refs = referenced_env(flows, args.seed) + missing = sorted(ref for ref in refs if not has_value(env, candidates_for(ref, store))) + if missing: + checks.append(Check("fail", "missing required env vars: " + ", ".join("MAESTRO_" + ref for ref in missing))) + else: + checks.append(Check("ok", f"all {len(refs)} referenced WOO_* env value(s) are available")) + + if any(flow.name == "login_not_woo_store.yaml" for flow in flows): + wpcom_fallback = [ + has_value(env, candidates_for("WOO_NOT_A_WOO_STORE_WPCOM_EMAIL", store)), + has_value(env, candidates_for("WOO_NOT_A_WOO_STORE_WPCOM_PASSWORD", store)), + ] + if any(wpcom_fallback) and not all(wpcom_fallback): + checks.append(Check("fail", "not-Woo-store WP.com fallback requires both email and password")) + + jetpack_candidates = candidates_for("WOO_JETPACK_STORE_URL", store) + no_jetpack_candidates = candidates_for("WOO_NO_JETPACK_SITE_URL", store) + jetpack_url = next((env[name] for name in jetpack_candidates if env.get(name)), "") + no_jetpack_url = next((env[name] for name in no_jetpack_candidates if env.get(name)), "") + if jetpack_url and no_jetpack_url and url_host(jetpack_url) == url_host(no_jetpack_url): + checks.append(Check("fail", "selected Jetpack store URL matches the no-Jetpack site URL")) + + has_destructive_flow = any("destructive" in flow_tags(flow) for flow in flows) + if store == "shared" and has_destructive_flow: + if not args.seed: + checks.append(Check("fail", "shared destructive flows require --seed")) + shared_url = env.get("MAESTRO_WOO_SHARED_JETPACK_STORE_URL", "") + shared_host = url_host(shared_url) + if shared_host != SHARED_STORE_HOST: + checks.append( + Check( + "fail", + f"shared destructive host must be {SHARED_STORE_HOST}; " + f"configured host is {shared_host or ''}", + ) + ) + if not os.access(SEED_SCRIPT, os.X_OK): + checks.append(Check("fail", f"shared-store lock helper is not executable: {SEED_SCRIPT}")) + if not os.environ.get("CI") and not os.environ.get("BUILDKITE"): + checks.append(Check("fail", "shared destructive runs are refused outside CI")) + + devices = adb_devices() + if args.device: + checks.append(Check("ok" if args.device in devices else "fail", f"requested adb device {args.device} {'is connected' if args.device in devices else 'is not connected'}")) + else: + checks.append(Check("ok" if devices else "fail", f"{len(devices)} adb device(s) connected")) + + print("Maestro smoke doctor") + print(f" profile: {args.profile}") + print(f" store: {store}") + print(f" include: {','.join(include_tags) or ''}") + print(f" exclude: {','.join(exclude_tags) or ''}") + print(f" seed: {'yes' if args.seed else 'no'}") + print() + + failed = 0 + for check in checks: + marker = {"ok": "OK", "warn": "WARN", "fail": "FAIL"}[check.status] + print(f"[{marker}] {check.message}") + failed += int(check.status == "fail") + + if flows: + print() + print("Selected flows:") + for flow in flows: + print(f" - {flow.relative_to(REPO_ROOT)}") + + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.maestro/scripts/doctor.sh b/.maestro/scripts/doctor.sh new file mode 100755 index 000000000000..f19b6ab452b3 --- /dev/null +++ b/.maestro/scripts/doctor.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/doctor.py" "$@" diff --git a/.maestro/scripts/generate-strings-env.py b/.maestro/scripts/generate-strings-env.py new file mode 100755 index 000000000000..3b731d881c37 --- /dev/null +++ b/.maestro/scripts/generate-strings-env.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Generate Maestro string environment variables from Android strings.xml.""" + +from __future__ import annotations + +import argparse +import re +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + + +FORMAT_RE = re.compile(r"%(?:\d+\$)?[dsf]") +NON_WORD_RE = re.compile(r"[^A-Za-z0-9]+") + + +def env_name(prefix: str, key: str) -> str: + normalized = NON_WORD_RE.sub("_", key).strip("_").upper() + return f"{prefix}_{normalized}" + + +def text_content(element: ET.Element) -> str: + return "".join(element.itertext()).strip() + + +def unescape_android(value: str) -> str: + return ( + value.replace("\\'", "'") + .replace('\\"', '"') + .replace("\\n", "\n") + .replace("\\@", "@") + .replace("\\?", "?") + ) + + +def to_regex(value: str) -> str: + parts: list[str] = [] + last = 0 + for match in FORMAT_RE.finditer(value): + parts.append(re.escape(value[last : match.start()])) + parts.append(".*") + last = match.end() + parts.append(re.escape(value[last:])) + return "".join(parts) + + +def shell_quote(value: str) -> str: + return "'" + value.replace("'", "'\"'\"'").replace("\n", "\\n") + "'" + + +def generate(strings_xml: Path, output: Path, prefix: str) -> None: + root = ET.parse(strings_xml).getroot() + lines = [ + "# Generated by .maestro/scripts/generate-strings-env.py", + f"# Source: {strings_xml}", + "", + ] + parameterized: list[str] = [] + + for item in root.findall("string"): + name = item.attrib.get("name") + if not name or item.attrib.get("translatable") == "false": + continue + value = unescape_android(text_content(item)) + if not value: + continue + variable = env_name(prefix, name) + if FORMAT_RE.search(value): + lines.append(f"{variable}_RE={shell_quote(to_regex(value))}") + parameterized.append(variable) + else: + lines.append(f"{variable}={shell_quote(value)}") + + for plural in root.findall("plurals"): + name = plural.attrib.get("name") + if not name: + continue + for item in plural.findall("item"): + quantity = item.attrib.get("quantity") + if not quantity: + continue + value = unescape_android(text_content(item)) + variable = env_name(prefix, f"{name}_{quantity}_RE") + lines.append(f"{variable}={shell_quote(to_regex(value))}") + + if parameterized: + lines.extend( + [ + "", + "# Parameterized strings intentionally emit only *_RE variables.", + "# Do not assert raw % placeholders in flows.", + ] + ) + for variable in parameterized: + lines.append(f"# {variable} -> {variable}_RE") + + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def verify_no_parameterized_literal_references(output: Path, flows_dir: Path) -> None: + env_text = output.read_text(encoding="utf-8") + parameterized = { + line.split(" -> ", 1)[0].removeprefix("# ") + for line in env_text.splitlines() + if line.startswith("# STRING_") and " -> " in line + } + if not parameterized: + return + errors: list[str] = [] + for flow in flows_dir.glob("*.yaml"): + text = flow.read_text(encoding="utf-8") + for variable in sorted(parameterized): + if "${" + variable + "}" in text: + errors.append(f"{flow}: references parameterized literal {variable}; use {variable}_RE") + if errors: + print("\n".join(errors), file=sys.stderr) + raise SystemExit(1) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--strings-xml", + default="WooCommerce/src/main/res/values/strings.xml", + type=Path, + ) + parser.add_argument("--output", default=".maestro/strings.env", type=Path) + parser.add_argument("--flows-dir", default=".maestro/flows", type=Path) + parser.add_argument("--prefix", default="STRING") + parser.add_argument("--check-flow-references", action="store_true") + args = parser.parse_args() + + generate(args.strings_xml, args.output, args.prefix) + if args.check_flow_references: + verify_no_parameterized_literal_references(args.output, args.flows_dir) + print(f"Wrote {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.maestro/scripts/lint-env.py b/.maestro/scripts/lint-env.py new file mode 100755 index 000000000000..da06e9352e56 --- /dev/null +++ b/.maestro/scripts/lint-env.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Lint local Maestro env files without printing secret values.""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +from pathlib import Path + + +ASSIGNMENT_RE = re.compile(r"^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$") +INLINE_COMMENT_RE = re.compile(r"\s+#") +SHELL_META_RE = re.compile(r"""[\s()&;<>|`$]""") +DEPRECATED_ALIASES = { + "MAESTRO_WOO_LAB_STORE_URL": "MAESTRO_WOO_LAB_JETPACK_STORE_URL", + "MAESTRO_WOO_LAB_EMAIL": "MAESTRO_WOO_LAB_WPCOM_EMAIL", + "MAESTRO_WOO_LAB_PASSWORD": "MAESTRO_WOO_LAB_WPCOM_PASSWORD", + "MAESTRO_WOO_SHARED_STORE_URL": "MAESTRO_WOO_SHARED_JETPACK_STORE_URL", + "MAESTRO_WOO_SHARED_EMAIL": "MAESTRO_WOO_SHARED_WPCOM_EMAIL", + "MAESTRO_WOO_SHARED_PASSWORD": "MAESTRO_WOO_SHARED_WPCOM_PASSWORD", + "MAESTRO_WOO_JN_SITE_URL": "MAESTRO_WOO_NO_JETPACK_SITE_URL", + "MAESTRO_WOO_JN_USERNAME": "MAESTRO_WOO_NO_JETPACK_SITE_ADMIN_USERNAME", + "MAESTRO_WOO_JN_PASSWORD": "MAESTRO_WOO_NO_JETPACK_SITE_ADMIN_PASSWORD", +} + + +def expected_names(example_path: Path) -> set[str]: + names: set[str] = set() + if not example_path.exists(): + return names + for line in example_path.read_text(errors="replace").splitlines(): + match = ASSIGNMENT_RE.match(line.strip()) + if match: + names.add(match.group(1)) + return names + + +def strip_inline_comment(value: str) -> str: + split = INLINE_COMMENT_RE.split(value, maxsplit=1) + return split[0].rstrip() + + +def is_quoted(value: str) -> bool: + value = value.strip() + return len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'} + + +def lint(path: Path, example_path: Path, seed: bool) -> tuple[list[str], list[str], int]: + errors: list[str] = [] + warnings: list[str] = [] + count = 0 + expected = expected_names(example_path) + + if not path.exists(): + return [f"{path} does not exist."], warnings, count + + syntax = subprocess.run(["bash", "-n", str(path)], capture_output=True, text=True) + if syntax.returncode != 0: + errors.append( + f"{path}: shell syntax check failed. Check quoting around passwords or values with shell metacharacters." + ) + + seen: set[str] = set() + for line_number, raw_line in enumerate(path.read_text(errors="replace").splitlines(), start=1): + stripped = raw_line.strip() + if not stripped or stripped.startswith("#"): + continue + + match = ASSIGNMENT_RE.match(stripped) + if not match: + errors.append(f"{path}:{line_number}: expected NAME=value or export NAME=value.") + continue + + name, raw_value = match.groups() + count += 1 + seen.add(name) + + value = strip_inline_comment(raw_value) + if name.startswith("MAESTRO_WOO_") and expected and name not in expected and name not in DEPRECATED_ALIASES: + warnings.append(f"{path}:{line_number}: {name} is not declared in .maestro/env.example.") + + if name in DEPRECATED_ALIASES: + warnings.append(f"{path}:{line_number}: {name} is supported as a legacy alias; prefer {DEPRECATED_ALIASES[name]}.") + + if not seed and re.search(r"MAESTRO_WOO_.*CONSUMER_(KEY|SECRET)$", name): + warnings.append(f"{path}:{line_number}: {name} is only needed when running with --seed.") + + if value and not is_quoted(value) and SHELL_META_RE.search(value): + errors.append( + f"{path}:{line_number}: {name} has an unquoted value containing shell metacharacters; wrap it in single quotes." + ) + + for name in sorted(seen): + if name.startswith("MAESTRO_WOO_") and "PASSWORD" in name: + # This intentionally does not inspect or print the value. The line-level checks above catch + # shell-unsafe password characters before the file is sourced. + continue + + return errors, warnings, count + + +def main() -> int: + parser = argparse.ArgumentParser(description="Lint .maestro/.env.local without printing secret values.") + parser.add_argument("--file", default=".maestro/.env.local", type=Path) + parser.add_argument("--example", default=".maestro/env.example", type=Path) + parser.add_argument("--seed", action="store_true", help="Allow Woo REST consumer key/secret variables.") + args = parser.parse_args() + + errors, warnings, count = lint(args.file, args.example, args.seed) + for warning in warnings: + print(f"warning: {warning}", file=sys.stderr) + if errors: + for error in errors: + print(f"error: {error}", file=sys.stderr) + return 1 + + print(f"OK: {args.file} contains {count} assignment(s).") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.maestro/scripts/run-smoke-tests.sh b/.maestro/scripts/run-smoke-tests.sh new file mode 100755 index 000000000000..9a1e19b86b6c --- /dev/null +++ b/.maestro/scripts/run-smoke-tests.sh @@ -0,0 +1,1329 @@ +#!/bin/bash +set -euo pipefail + +# WooCommerce Android Maestro smoke-test runner. +# +# Defaults match the v2 smoke-test plan: +# - lab store by default +# - smoke_core only by default +# - flaky_quarantine excluded unless explicitly requested +# - no REST fixture seed unless --seed is passed +# - animation settings captured and restored +# - one retry per failed flow, recorded as flaky + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +FLOWS_DIR="$REPO_ROOT/.maestro/flows" +ENV_FILE="$REPO_ROOT/.maestro/.env.local" +STRINGS_ENV_FILE="$REPO_ROOT/.maestro/strings.env" +SEED_SCRIPT="${WOO_MAESTRO_SEED_SCRIPT:-$REPO_ROOT/.maestro/scripts/seed-fixtures.py}" +PLAN_SCRIPT="$REPO_ROOT/.maestro/scripts/smoke_plan.py" +CHECK_TOOLCHAIN_SCRIPT="$REPO_ROOT/.maestro/scripts/check-toolchain.py" +SHARED_STORE_HOST="inpersonpayments.wpcomstaging.com" + +RUN_STAMP="$(date +%Y%m%d%H%M%S)" +RUN_HASH="$(printf '%s-%s-%s' "$RUN_STAMP" "$$" "${RANDOM:-0}" | cksum | awk '{print $1}')" +SUITE_RUN_ID="SUITE-${RUN_STAMP}-${RUN_HASH}" +TIMESTAMP="$RUN_STAMP" + +DEFAULT_OUTPUT_ROOT="${WOO_MAESTRO_OUTPUT_DIR:-$HOME/woocommerce-maestro-output}" +STORE="lab" +APK_PATH="" +DEVICE_SELECTOR="" +TARGET="" +PROFILE="" +RERUN_FAILED_FILE="" +OPEN_REPORT="auto" +RECORD="yes" +SEED="no" +CLEANUP="yes" +SWEEP_DRY_RUN="no" +OUTPUT_ROOT="" +REPEAT=1 +INCLUDE_TAGS=("smoke_core") +EXCLUDE_TAGS=("flaky_quarantine") +INCLUDE_TAGS_EXPLICIT="no" +EXCLUDE_TAGS_EXPLICIT="no" +INCLUDE_QUARANTINE="no" +PLAN="no" + +usage() { + cat <<'USAGE' +WooCommerce Android Maestro smoke-test runner. + +Defaults: + - lab store + - smoke_core only + - flaky_quarantine excluded unless explicitly requested + - no REST fixture seed unless --seed is passed + - animation settings captured and restored + - one retry per failed flow, recorded as flaky + +Usage: + .maestro/scripts/run-smoke-tests.sh + .maestro/scripts/run-smoke-tests.sh --profile core + .maestro/scripts/run-smoke-tests.sh --profile phone-full --device emulator-5554 + .maestro/scripts/run-smoke-tests.sh --include-tags smoke_extended --include-quarantine --store lab + .maestro/scripts/run-smoke-tests.sh --store shared --include-tags smoke_core + .maestro/scripts/run-smoke-tests.sh --device emulator-5554 --apk path/to/app.apk + .maestro/scripts/run-smoke-tests.sh --repeat 5 --store shared --include-tags smoke_core,smoke_extended + .maestro/scripts/run-smoke-tests.sh --rerun-failed path/to/report.xml --store lab + .maestro/scripts/run-smoke-tests.sh .maestro/flows/orders_list_and_search.yaml + +Options: + --profile name Preset: core, phone-full, release, burst, pos-tablet, android-system. + --store lab|shared Select fixture/credential namespace. Default: lab. + --device serial|avd-name Device serial or emulator AVD name. + --apk path Install APK before running. + --repeat N Run the selected flow set N times. + -t, --tag tag Alias for --include-tags. + --include-tags a,b Include flows with any listed tag. Default: smoke_core. + --exclude-tags a,b Exclude flows with any listed tag. Default: flaky_quarantine. + --include-quarantine Remove flaky_quarantine from the active exclusions. + --rerun-failed report.xml Rerun only testcases with failure/error in a previous JUnit report. + --seed Run REST fixture seed/cleanup. Requires Woo REST API credentials. + --no-seed Skip REST fixture seed/cleanup. + --no-cleanup Leave seeded manifest entities behind. + --sweep-dry-run Log stale-orphan sweep candidates without deleting. + --no-record Disable failure videos. + --no-open Do not open the HTML report on macOS. + --output-dir path Override output root. + --plan Print the resolved selection without touching tools, credentials, or devices. +USAGE +} + +add_csv_tags() { + local target_name="$1" + local csv="$2" + [[ -z "$csv" ]] && return + local old_ifs="$IFS" + IFS="," + read -r -a parts <<< "$csv" + IFS="$old_ifs" + local part + for part in "${parts[@]}"; do + part="$(printf '%s' "$part" | xargs)" + [[ -z "$part" ]] && continue + eval "$target_name+=(\"\$part\")" + done +} + +apply_profile() { + PROFILE="$1" + local profile_output key value + if ! profile_output="$(python3 "$PLAN_SCRIPT" profile "$PROFILE")"; then + exit 2 + fi + INCLUDE_TAGS=() + EXCLUDE_TAGS=() + while IFS=$'\t' read -r key value; do + case "$key" in + store) STORE="$value" ;; + repeat) REPEAT="$value" ;; + include) add_csv_tags INCLUDE_TAGS "$value" ;; + exclude) add_csv_tags EXCLUDE_TAGS "$value" ;; + esac + done <<< "$profile_output" + INCLUDE_TAGS_EXPLICIT="yes" + EXCLUDE_TAGS_EXPLICIT="yes" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --profile) + apply_profile "${2:?--profile requires a profile name}" + shift 2 + ;; + --rerun-failed) + RERUN_FAILED_FILE="${2:?--rerun-failed requires a JUnit report path}" + shift 2 + ;; + --apk) + APK_PATH="${2:?--apk requires a path}" + shift 2 + ;; + --device) + DEVICE_SELECTOR="${2:?--device requires a serial or AVD name}" + shift 2 + ;; + --store) + STORE="${2:?--store requires lab or shared}" + shift 2 + ;; + --repeat) + REPEAT="${2:?--repeat requires a number}" + shift 2 + ;; + --output-dir) + OUTPUT_ROOT="${2:?--output-dir requires a path}" + shift 2 + ;; + --plan) + PLAN="yes" + shift + ;; + -t|--tag|--include-tags) + if [[ "$INCLUDE_TAGS_EXPLICIT" == "no" ]]; then + INCLUDE_TAGS=() + INCLUDE_TAGS_EXPLICIT="yes" + fi + add_csv_tags INCLUDE_TAGS "${2:?--include-tags requires a tag}" + shift 2 + ;; + --exclude-tags) + if [[ "$EXCLUDE_TAGS_EXPLICIT" == "no" ]]; then + EXCLUDE_TAGS=() + EXCLUDE_TAGS_EXPLICIT="yes" + fi + add_csv_tags EXCLUDE_TAGS "${2:?--exclude-tags requires a tag}" + shift 2 + ;; + --include-quarantine) + INCLUDE_QUARANTINE="yes" + shift + ;; + --no-seed) + SEED="no" + shift + ;; + --seed) + SEED="yes" + shift + ;; + --no-cleanup) + CLEANUP="no" + shift + ;; + --sweep-dry-run) + SWEEP_DRY_RUN="yes" + shift + ;; + --no-record) + RECORD="no" + shift + ;; + --no-open) + OPEN_REPORT="no" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + if [[ -n "$TARGET" ]]; then + echo "Unexpected argument: $1" >&2 + exit 2 + fi + TARGET="$1" + shift + ;; + esac +done + +if [[ -n "$RERUN_FAILED_FILE" && -n "$TARGET" ]]; then + echo "--rerun-failed cannot be combined with a positional flow target." >&2 + exit 2 +fi +if [[ "$STORE" != "lab" && "$STORE" != "shared" ]]; then + echo "--store must be lab or shared" >&2 + exit 2 +fi +if ! [[ "$REPEAT" =~ ^[0-9]+$ ]] || [[ "$REPEAT" -lt 1 ]]; then + echo "--repeat must be a positive integer" >&2 + exit 2 +fi +if [[ "$INCLUDE_TAGS_EXPLICIT" == "yes" ]]; then + for tag in "${INCLUDE_TAGS[@]}"; do + if [[ "$tag" == "flaky_quarantine" && "$EXCLUDE_TAGS_EXPLICIT" == "no" ]]; then + EXCLUDE_TAGS=() + fi + done +fi +if [[ "$INCLUDE_QUARANTINE" == "yes" ]]; then + FILTERED_EXCLUDE_TAGS_CSV="" + for tag in "${EXCLUDE_TAGS[@]}"; do + if [[ "$tag" != "flaky_quarantine" ]]; then + FILTERED_EXCLUDE_TAGS_CSV="${FILTERED_EXCLUDE_TAGS_CSV:+$FILTERED_EXCLUDE_TAGS_CSV,}$tag" + fi + done + EXCLUDE_TAGS=() + if [[ -n "$FILTERED_EXCLUDE_TAGS_CSV" ]]; then + add_csv_tags EXCLUDE_TAGS "$FILTERED_EXCLUDE_TAGS_CSV" + fi +fi + +join_tags_csv() { + local old_ifs="$IFS" + IFS="," + printf '%s' "$*" + IFS="$old_ifs" +} + +if [[ "$PLAN" == "yes" ]]; then + if [[ -n "$RERUN_FAILED_FILE" || -n "$TARGET" ]]; then + echo "--plan currently supports profile/tag selections only." >&2 + exit 2 + fi + INCLUDE_TAGS_CSV="" + EXCLUDE_TAGS_CSV="" + if [[ ${#INCLUDE_TAGS[@]} -gt 0 ]]; then + INCLUDE_TAGS_CSV="$(join_tags_csv "${INCLUDE_TAGS[@]}")" + fi + if [[ ${#EXCLUDE_TAGS[@]} -gt 0 ]]; then + EXCLUDE_TAGS_CSV="$(join_tags_csv "${EXCLUDE_TAGS[@]}")" + fi + plan_args=( + plan + --profile-label "$PROFILE" + --store "$STORE" + --repeat "$REPEAT" + --include-tags "$INCLUDE_TAGS_CSV" + --exclude-tags "$EXCLUDE_TAGS_CSV" + ) + if [[ "$SEED" == "yes" ]]; then + plan_args+=(--seed) + fi + exec python3 "$PLAN_SCRIPT" "${plan_args[@]}" +fi + +OUTPUT_ROOT="${OUTPUT_ROOT:-$DEFAULT_OUTPUT_ROOT}" +OUTPUT_DIR="$OUTPUT_ROOT/$TIMESTAMP" +RECORDINGS_DIR="$OUTPUT_DIR/recordings" +SCREENSHOTS_DIR="$OUTPUT_DIR/screenshots" +LOGS_DIR="$OUTPUT_DIR/logs" +TMP_DIR="$OUTPUT_DIR/tmp" +REPORT_FILE="$OUTPUT_DIR/report.html" +JUNIT_FILE="$OUTPUT_DIR/report.xml" +MANIFEST_FILE="$TMP_DIR/run-manifest.json" +RUN_ENV_FILE="$TMP_DIR/run-env.sh" +SWEEP_REPORT="$TMP_DIR/orphan-sweep.json" + +export MAESTRO_SUITE_RUN_ID="$SUITE_RUN_ID" +mkdir -p "$LOGS_DIR" "$TMP_DIR" "$SCREENSHOTS_DIR" + +echo "--- Pre-flight checks" +if ! command -v maestro >/dev/null 2>&1; then + echo "maestro CLI not found. Install the repository pin: MAESTRO_VERSION=2.8.0 (see .maestro/README.md)." >&2 + exit 1 +fi +if ! command -v adb >/dev/null 2>&1; then + echo "adb not found. Ensure Android SDK platform-tools is on PATH." >&2 + exit 1 +fi +python3 "$CHECK_TOOLCHAIN_SCRIPT" + +if [[ -f "$ENV_FILE" ]]; then + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a +fi +if [[ -f "$STRINGS_ENV_FILE" ]]; then + set -a + # shellcheck disable=SC1090 + source "$STRINGS_ENV_FILE" + set +a +fi + +map_store_env() { + local target="$1" + shift + local source value + for source in "$@"; do + value="${!source:-}" + if [[ -n "$value" ]]; then + export "$target=$value" + return + fi + done +} + +alias_env() { + local target="$1" + local source="$2" + if [[ -z "${!target:-}" && -n "${!source:-}" ]]; then + export "$target=${!source}" + fi +} + +select_store_env() { + local upper + upper="$(printf '%s' "$STORE" | tr '[:lower:]' '[:upper:]')" + unset \ + MAESTRO_WOO_JETPACK_STORE_URL \ + MAESTRO_WOO_WPCOM_EMAIL \ + MAESTRO_WOO_WPCOM_PASSWORD \ + MAESTRO_WOO_STORE_URL \ + MAESTRO_WOO_EMAIL \ + MAESTRO_WOO_PASSWORD \ + MAESTRO_WOO_CONSUMER_KEY \ + MAESTRO_WOO_CONSUMER_SECRET + map_store_env \ + MAESTRO_WOO_JETPACK_STORE_URL \ + "MAESTRO_WOO_${upper}_JETPACK_STORE_URL" \ + "MAESTRO_WOO_${upper}_STORE_URL" + map_store_env MAESTRO_WOO_WPCOM_EMAIL "MAESTRO_WOO_${upper}_WPCOM_EMAIL" "MAESTRO_WOO_${upper}_EMAIL" + map_store_env \ + MAESTRO_WOO_WPCOM_PASSWORD \ + "MAESTRO_WOO_${upper}_WPCOM_PASSWORD" \ + "MAESTRO_WOO_${upper}_PASSWORD" + + alias_env MAESTRO_WOO_STORE_URL MAESTRO_WOO_JETPACK_STORE_URL + alias_env MAESTRO_WOO_EMAIL MAESTRO_WOO_WPCOM_EMAIL + alias_env MAESTRO_WOO_PASSWORD MAESTRO_WOO_WPCOM_PASSWORD + + map_store_env MAESTRO_WOO_CONSUMER_KEY "MAESTRO_WOO_${upper}_CONSUMER_KEY" + map_store_env MAESTRO_WOO_CONSUMER_SECRET "MAESTRO_WOO_${upper}_CONSUMER_SECRET" + + alias_env MAESTRO_WOO_NO_JETPACK_SITE_URL MAESTRO_WOO_JN_SITE_URL + alias_env MAESTRO_WOO_NO_JETPACK_SITE_ADMIN_USERNAME MAESTRO_WOO_JN_USERNAME + alias_env MAESTRO_WOO_NO_JETPACK_SITE_ADMIN_PASSWORD MAESTRO_WOO_JN_PASSWORD +} +select_store_env + +flow_tags() { + awk ' + /^---/ { exit } + /^tags:/ { in_tags = 1; next } + in_tags && /^[[:space:]]*-[[:space:]]*/ { + gsub(/^[[:space:]]*-[[:space:]]*/, "", $0) + gsub(/[[:space:]]+$/, "", $0) + print $0 + next + } + in_tags && /^[^[:space:]]/ { in_tags = 0 } + ' "$1" +} + +flow_has_any_tag() { + local flow="$1" + shift + [[ $# -eq 0 ]] && return 1 + local tags tag wanted + tags="$(flow_tags "$flow")" + for wanted in "$@"; do + while IFS= read -r tag; do + [[ "$tag" == "$wanted" ]] && return 0 + done <<< "$tags" + done + return 1 +} + +url_host() { + local value="${1:-}" + value="${value#http://}" + value="${value#https://}" + value="${value%%/*}" + value="${value%%:*}" + printf '%s' "$value" | tr '[:upper:]' '[:lower:]' +} + +flow_uses_wpcom_credentials() { + local flow name + local wpcom_ref_pattern + wpcom_ref_pattern='\$\{WOO_(JETPACK_STORE_URL|WPCOM_EMAIL|WPCOM_PASSWORD)\}' + for flow in "${ORDERED_FLOWS[@]}"; do + name="$(basename "$flow")" + case "$name" in + login_help.yaml|login_no_jetpack.yaml|login_not_wp_site.yaml) + continue + ;; + esac + if grep -Eq "$wpcom_ref_pattern|subflows/(ensure_logged_in|login)\\.yaml" "$flow"; then + return 0 + fi + done + return 1 +} + +validate_login_store_env() { + flow_uses_wpcom_credentials || return 0 + + local selected_host no_jetpack_host upper + upper="$(printf '%s' "$STORE" | tr '[:lower:]' '[:upper:]')" + selected_host="$(url_host "${MAESTRO_WOO_JETPACK_STORE_URL:-}")" + no_jetpack_host="$(url_host "${MAESTRO_WOO_NO_JETPACK_SITE_URL:-}")" + + if [[ -n "$selected_host" && -n "$no_jetpack_host" && "$selected_host" == "$no_jetpack_host" ]]; then + cat >&2 <&2 + exit 1 + fi + python3 - "$report" <<'PY' +import pathlib +import sys +import xml.etree.ElementTree as ET + +path = pathlib.Path(sys.argv[1]) +try: + root = ET.parse(path).getroot() +except ET.ParseError as error: + raise SystemExit(f"Could not parse JUnit report {path}: {error}") + +seen = set() +for testcase in root.iter("testcase"): + if testcase.find("failure") is None and testcase.find("error") is None: + continue + name = testcase.attrib.get("name", "").strip() + if not name or name in seen: + continue + seen.add(name) + print(name) +PY +} + +ORDERED_FLOWS=() +if [[ -n "$RERUN_FAILED_FILE" ]]; then + RERUN_FAILED_NAMES=() + while IFS= read -r name; do + [[ -z "$name" ]] && continue + RERUN_FAILED_NAMES+=("$name") + done < <(read_failed_flow_names "$RERUN_FAILED_FILE") + if [[ ${#RERUN_FAILED_NAMES[@]} -eq 0 ]]; then + echo "No failed or flaky testcases found in $RERUN_FAILED_FILE." + exit 0 + fi + for name in "${RERUN_FAILED_NAMES[@]}"; do + name="${name%.yaml}.yaml" + flow_path="$FLOWS_DIR/$name" + if [[ ! -f "$flow_path" ]]; then + echo "Failed testcase does not map to a flow file: $name" >&2 + exit 1 + fi + ORDERED_FLOWS+=("$flow_path") + done +elif [[ -n "$TARGET" ]]; then + if [[ ! -f "$TARGET" ]]; then + echo "Target flow not found: $TARGET" >&2 + exit 1 + fi + ORDERED_FLOWS+=("$(cd "$(dirname "$TARGET")" && pwd)/$(basename "$TARGET")") +else + INCLUDE_TAGS_CSV="" + EXCLUDE_TAGS_CSV="" + if [[ ${#INCLUDE_TAGS[@]} -gt 0 ]]; then + INCLUDE_TAGS_CSV="$(join_tags_csv "${INCLUDE_TAGS[@]}")" + fi + if [[ ${#EXCLUDE_TAGS[@]} -gt 0 ]]; then + EXCLUDE_TAGS_CSV="$(join_tags_csv "${EXCLUDE_TAGS[@]}")" + fi + if ! SELECTION_OUTPUT="$( + python3 "$PLAN_SCRIPT" select \ + --include-tags "$INCLUDE_TAGS_CSV" \ + --exclude-tags "$EXCLUDE_TAGS_CSV" + )"; then + exit 1 + fi + while IFS= read -r flow_path; do + [[ -n "$flow_path" ]] && ORDERED_FLOWS+=("$flow_path") + done <<< "$SELECTION_OUTPUT" +fi +if [[ ${#ORDERED_FLOWS[@]} -eq 0 ]]; then + echo "No flows matched the current filters." >&2 + exit 1 +fi +validate_login_store_env + +SUITE_HAS_DESTRUCTIVE="no" +for flow in "${ORDERED_FLOWS[@]}"; do + if flow_has_any_tag "$flow" destructive; then + SUITE_HAS_DESTRUCTIVE="yes" + fi +done +if [[ "$SEED" == "yes" && "$SUITE_HAS_DESTRUCTIVE" != "yes" ]]; then + echo "No destructive flows selected; skipping fixture seeding." + SEED="no" +fi +if [[ "$STORE" == "shared" && "$SUITE_HAS_DESTRUCTIVE" == "yes" && -z "${CI:-}" && -z "${BUILDKITE:-}" ]]; then + echo "Refusing to run destructive flows against the shared store outside CI." >&2 + echo "Use --store lab for destructive iteration, or remove destructive flows from the selection." >&2 + exit 1 +fi +if [[ "$STORE" == "shared" && "$SUITE_HAS_DESTRUCTIVE" == "yes" && "$SEED" != "yes" ]]; then + echo "Shared destructive runs require --seed so fixtures and the store lock are mandatory." >&2 + exit 1 +fi + +validate_shared_destructive_config() { + [[ "$STORE" == "shared" && "$SUITE_HAS_DESTRUCTIVE" == "yes" ]] || return 0 + + local required missing=() name + for required in \ + MAESTRO_WOO_SHARED_JETPACK_STORE_URL \ + MAESTRO_WOO_SHARED_WPCOM_EMAIL \ + MAESTRO_WOO_SHARED_WPCOM_PASSWORD \ + MAESTRO_WOO_SHARED_CONSUMER_KEY \ + MAESTRO_WOO_SHARED_CONSUMER_SECRET; do + if [[ -z "${!required:-}" ]]; then + missing+=("$required") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + echo "Missing scoped shared store configuration:" >&2 + for name in "${missing[@]}"; do + echo " - $name" >&2 + done + exit 1 + fi + + local configured_host + configured_host="$(url_host "$MAESTRO_WOO_SHARED_JETPACK_STORE_URL")" + if [[ "$configured_host" != "$SHARED_STORE_HOST" ]]; then + echo "Shared destructive runs require host $SHARED_STORE_HOST; configured host is ${configured_host:-}." >&2 + exit 1 + fi +} +validate_shared_destructive_config + +is_optional_flow_env_ref() { + local flow="$1" + local ref="$2" + [[ "$(basename "$flow")" == "login_not_woo_store.yaml" ]] && + [[ "$ref" == "WOO_NOT_A_WOO_STORE_WPCOM_EMAIL" || + "$ref" == "WOO_NOT_A_WOO_STORE_WPCOM_PASSWORD" ]] +} + +validate_referenced_env() { + local missing=() + local flow ref var + for flow in "${ORDERED_FLOWS[@]}"; do + while IFS= read -r ref; do + [[ -z "$ref" ]] && continue + is_optional_flow_env_ref "$flow" "$ref" && continue + var="MAESTRO_${ref}" + if [[ -z "${!var:-}" ]]; then + missing+=("$var") + fi + done < <(grep -Eoh '\$\{WOO_[A-Z0-9_]+\}' "$flow" | sed 's/[${}]//g' | sort -u) + done + if flow_uses_wpcom_credentials; then + for var in MAESTRO_WOO_JETPACK_STORE_URL MAESTRO_WOO_WPCOM_EMAIL MAESTRO_WOO_WPCOM_PASSWORD; do + if [[ -z "${!var:-}" ]]; then + missing+=("$var") + fi + done + fi + if [[ ${#missing[@]} -gt 0 ]]; then + printf '%s\n' "${missing[@]}" | sort -u | sed 's/^/Missing required env var: /' >&2 + echo "Populate $ENV_FILE from .maestro/env.example. Values are intentionally not echoed." >&2 + exit 1 + fi +} +validate_referenced_env + +validate_optional_not_woo_wpcom_env() { + local flow selected="no" + for flow in "${ORDERED_FLOWS[@]}"; do + if [[ "$(basename "$flow")" == "login_not_woo_store.yaml" ]]; then + selected="yes" + break + fi + done + [[ "$selected" == "yes" ]] || return 0 + + local email="${MAESTRO_WOO_NOT_A_WOO_STORE_WPCOM_EMAIL:-}" + local password="${MAESTRO_WOO_NOT_A_WOO_STORE_WPCOM_PASSWORD:-}" + if [[ -n "$email" && -z "$password" ]] || [[ -z "$email" && -n "$password" ]]; then + echo "Missing optional WP.com fallback pair: set both MAESTRO_WOO_NOT_A_WOO_STORE_WPCOM_EMAIL and MAESTRO_WOO_NOT_A_WOO_STORE_WPCOM_PASSWORD, or leave both blank." >&2 + exit 1 + fi +} +validate_optional_not_woo_wpcom_env + +LOCK_ACQUIRED="no" +SETTINGS_CAPTURED="no" +RECORDER_PID="" +CLEANUP_DONE="no" +CLEANUP_STATUS="NOT_REQUESTED" +CLEANUP_ERROR="" + +release_shared_lock() { + if [[ "$LOCK_ACQUIRED" == "yes" && -f "$MANIFEST_FILE" ]]; then + "$SEED_SCRIPT" unlock --manifest "$MANIFEST_FILE" --store shared || true + LOCK_ACQUIRED="no" + fi +} + +trap release_shared_lock EXIT +if [[ "$STORE" == "shared" && "$SUITE_HAS_DESTRUCTIVE" == "yes" ]]; then + if [[ ! -x "$SEED_SCRIPT" ]]; then + echo "Shared destructive lock helper is not executable: $SEED_SCRIPT" >&2 + exit 1 + fi + echo "--- Acquiring shared-store destructive lock" + "$SEED_SCRIPT" lock --store shared --run-id "$SUITE_RUN_ID" --manifest "$MANIFEST_FILE" >/dev/null + LOCK_ACQUIRED="yes" +fi + +DEVICE_SERIALS=() +while read -r serial state _rest; do + if [[ "${state:-}" == "device" ]]; then + DEVICE_SERIALS+=("$serial") + fi +done < <(adb devices) + +resolve_device() { + local selector="$1" + local serial avd + if [[ ${#DEVICE_SERIALS[@]} -eq 0 ]]; then + echo "No Android device/emulator connected. Start one and retry." >&2 + exit 1 + fi + if [[ -n "$selector" ]]; then + for serial in "${DEVICE_SERIALS[@]}"; do + if [[ "$serial" == "$selector" ]]; then + printf '%s' "$serial" + return + fi + avd="$(adb -s "$serial" emu avd name 2>/dev/null | tr -d '\r' | head -n 1 || true)" + if [[ "$avd" == "$selector" ]]; then + printf '%s' "$serial" + return + fi + done + echo "No attached device matched --device $selector" >&2 + exit 1 + fi + if [[ ${#DEVICE_SERIALS[@]} -eq 1 ]]; then + printf '%s' "${DEVICE_SERIALS[0]}" + return + fi + echo "Multiple Android devices are connected:" >&2 + local index=1 + for serial in "${DEVICE_SERIALS[@]}"; do + avd="$(adb -s "$serial" emu avd name 2>/dev/null | tr -d '\r' | head -n 1 || true)" + echo " $index) $serial ${avd:+($avd)}" >&2 + index=$((index + 1)) + done + if [[ -n "${CI:-}" || -n "${BUILDKITE:-}" || ! -t 0 ]]; then + echo "Pass --device when multiple devices are connected." >&2 + exit 1 + fi + read -r -p "Select device number: " selected + if ! [[ "$selected" =~ ^[0-9]+$ ]] || [[ "$selected" -lt 1 || "$selected" -gt ${#DEVICE_SERIALS[@]} ]]; then + echo "Invalid device selection." >&2 + exit 1 + fi + printf '%s' "${DEVICE_SERIALS[$((selected - 1))]}" +} + +DEVICE_SERIAL="$(resolve_device "$DEVICE_SELECTOR")" +MAESTRO_DEVICE_ARGS=(--device "$DEVICE_SERIAL") +echo "Device: $DEVICE_SERIAL" + +ANIMATION_KEYS=(window_animation_scale transition_animation_scale animator_duration_scale) +ORIGINAL_ANIMATION_VALUES=() + +capture_animation_settings() { + local key value + ORIGINAL_ANIMATION_VALUES=() + for key in "${ANIMATION_KEYS[@]}"; do + value="$(adb -s "$DEVICE_SERIAL" shell settings get global "$key" 2>/dev/null | tr -d '\r' || true)" + ORIGINAL_ANIMATION_VALUES+=("${value:-1}") + adb -s "$DEVICE_SERIAL" shell settings put global "$key" 0 >/dev/null + done + SETTINGS_CAPTURED="yes" +} + +restore_animation_settings() { + [[ "$SETTINGS_CAPTURED" == "yes" ]] || return 0 + local index key value + index=0 + for key in "${ANIMATION_KEYS[@]}"; do + value="${ORIGINAL_ANIMATION_VALUES[$index]:-1}" + adb -s "$DEVICE_SERIAL" shell settings put global "$key" "$value" >/dev/null 2>&1 || true + index=$((index + 1)) + done +} + +stop_screenrecord() { + adb -s "$DEVICE_SERIAL" shell "pkill -INT screenrecord 2>/dev/null || true" >/dev/null 2>&1 || true + sleep 1 +} + +collapse_system_ui() { + adb -s "$DEVICE_SERIAL" shell cmd statusbar collapse >/dev/null 2>&1 || true +} + +cleanup_on_exit() { + local exit_code=$? + if [[ -n "$RECORDER_PID" ]]; then + stop_screenrecord + wait "$RECORDER_PID" 2>/dev/null || true + fi + if [[ "$CLEANUP_DONE" != "yes" && "$CLEANUP" == "yes" && "$SEED" == "yes" && -f "$MANIFEST_FILE" ]]; then + if ! "$SEED_SCRIPT" cleanup --manifest "$MANIFEST_FILE" --store "$STORE"; then + exit_code=1 + fi + fi + release_shared_lock + restore_animation_settings + exit "$exit_code" +} +trap cleanup_on_exit EXIT INT TERM + +capture_animation_settings + +if [[ -n "$APK_PATH" ]]; then + if [[ ! -f "$APK_PATH" ]]; then + echo "APK not found at: $APK_PATH" >&2 + exit 1 + fi + echo "--- Installing APK" + adb -s "$DEVICE_SERIAL" install -r -g "$APK_PATH" +fi + +validate_google_login_apk() { + local flow google_flow_selected="no" + for flow in "${ORDERED_FLOWS[@]}"; do + if [[ "$(basename "$flow")" == "login_google.yaml" ]]; then + google_flow_selected="yes" + break + fi + done + [[ "$google_flow_selected" == "yes" ]] || return 0 + + local apk_to_check="$APK_PATH" + local pulled_apk="no" + if [[ -z "$apk_to_check" ]]; then + local installed_apk_path + installed_apk_path="$( + adb -s "$DEVICE_SERIAL" shell pm path com.woocommerce.android.dev 2>/dev/null | + tr -d '\r' | + sed -n '1s/^package://p' + )" + if [[ -z "$installed_apk_path" ]]; then + echo "Setup error: com.woocommerce.android.dev is not installed for login_google." >&2 + exit 1 + fi + apk_to_check="$TMP_DIR/login-google-installed.apk" + adb -s "$DEVICE_SERIAL" pull "$installed_apk_path" "$apk_to_check" >/dev/null + pulled_apk="yes" + fi + + local validation_status=0 + python3 - "$REPO_ROOT/WooCommerce/google-services.json-example" "$apk_to_check" <<'PY' || validation_status=$? +import json +import pathlib +import sys +import zipfile + +config = json.loads(pathlib.Path(sys.argv[1]).read_text()) +example_client_id = "" +for client in config.get("client", []): + package_name = client.get("client_info", {}).get("android_client_info", {}).get("package_name") + if package_name != "com.woocommerce.android.dev": + continue + for oauth_client in client.get("oauth_client", []): + if oauth_client.get("client_type") == 3: + example_client_id = oauth_client.get("client_id", "") + break + +if not example_client_id: + raise SystemExit(3) + +try: + with zipfile.ZipFile(sys.argv[2]) as apk: + resources = apk.read("resources.arsc") +except (KeyError, OSError, zipfile.BadZipFile): + raise SystemExit(3) + +raise SystemExit(2 if example_client_id.encode() in resources else 0) +PY + + if [[ "$pulled_apk" == "yes" ]]; then + rm -f "$apk_to_check" + fi + + if [[ "$validation_status" -eq 2 ]]; then + cat >&2 <<'EOF' +Setup error: login_google cannot run with the example Google OAuth client. + +Build or obtain the APK with the private WooCommerce google-services.json, +then install it or pass it with --apk. For a configured local checkout: + ./gradlew :WooCommerce:installWasabiDebug +EOF + exit 1 + fi + if [[ "$validation_status" -ne 0 ]]; then + echo "Setup error: could not inspect the APK resources required by login_google." >&2 + exit 1 + fi +} +validate_google_login_apk + +if [[ "$SEED" == "yes" ]]; then + echo "--- Stale automation orphan sweep" + sweep_args=(sweep --store "$STORE" --report "$SWEEP_REPORT") + if [[ "$SWEEP_DRY_RUN" == "yes" ]]; then + sweep_args+=(--dry-run) + fi + "$SEED_SCRIPT" "${sweep_args[@]}" + + echo "--- Seeding deterministic fixtures" + "$SEED_SCRIPT" seed --store "$STORE" --run-id "$SUITE_RUN_ID" --manifest "$MANIFEST_FILE" --env-file "$RUN_ENV_FILE" + # shellcheck disable=SC1090 + source "$RUN_ENV_FILE" +fi + +MAESTRO_ENV_ARGS=(-e "SUITE_RUN_ID=$SUITE_RUN_ID") +MAESTRO_PROCESS_ENV_ARGS=(env) +while IFS='=' read -r name _value; do + [[ -n "$name" ]] && MAESTRO_PROCESS_ENV_ARGS+=(-u "$name") +done < <(env | grep '^MAESTRO_WOO_' || true) + +FLOW_ENV_REFS=() +while IFS= read -r ref; do + [[ -n "$ref" ]] && FLOW_ENV_REFS+=("$ref") +done < <(grep -Eoh '\$\{WOO_[A-Z0-9_]+\}' "${ORDERED_FLOWS[@]}" | sed 's/[${}]//g' | sort -u || true) +if flow_uses_wpcom_credentials; then + FLOW_ENV_REFS+=(WOO_JETPACK_STORE_URL WOO_WPCOM_EMAIL WOO_WPCOM_PASSWORD) +fi +while IFS= read -r ref; do + [[ -z "$ref" ]] && continue + source_name="MAESTRO_$ref" + value="${!source_name:-}" + [[ -n "$value" ]] && MAESTRO_PROCESS_ENV_ARGS+=("$ref=$value") +done < <(printf '%s\n' "${FLOW_ENV_REFS[@]}" | sort -u) + +# Forward only the STRING_* variables the selected flows reference; forwarding +# all generated strings would risk exceeding ARG_MAX. +while IFS= read -r ref; do + [[ -z "$ref" ]] && continue + if [[ -z "${!ref:-}" ]]; then + echo "Missing generated string variable: $ref (regenerate .maestro/strings.env)" >&2 + exit 1 + fi + MAESTRO_ENV_ARGS+=(-e "${ref}=${!ref}") +done < <(grep -Eoh '\$\{STRING_[A-Z0-9_]+\}' "${ORDERED_FLOWS[@]}" 2>/dev/null | sed 's/[${}]//g' | sort -u || true) + +scrub_log() { + local file="$1" + python3 - "$file" <<'PY' +import os +import pathlib +import sys + +path = pathlib.Path(sys.argv[1]) +text = path.read_text(errors="replace") +for key, value in os.environ.items(): + if key.startswith("MAESTRO_WOO_") and value: + text = text.replace(value, "[REDACTED]") +path.write_text(text) +PY +} + +xml_escape() { + python3 -c 'import html,sys; print(html.escape(sys.stdin.read()), end="")' +} + +join_csv() { + local old_ifs="$IFS" + IFS="," + printf '%s' "$*" + IFS="$old_ifs" +} + +shell_arg() { + local value="$1" + if [[ "$value" =~ ^[A-Za-z0-9_./:=,+@%-]+$ ]]; then + printf '%s' "$value" + else + printf "'%s'" "$(printf '%s' "$value" | sed "s/'/'\\\\''/g")" + fi +} + +render_command() { + local first="yes" + local arg + for arg in "$@"; do + if [[ "$first" == "yes" ]]; then + first="no" + else + printf ' ' + fi + shell_arg "$arg" + done +} + +selection_args() { + if [[ -n "$PROFILE" ]]; then + printf '%s\0%s\0' --profile "$PROFILE" + else + local include_csv="" exclude_csv="" + if [[ ${#INCLUDE_TAGS[@]} -gt 0 ]]; then + include_csv="$(join_csv "${INCLUDE_TAGS[@]}")" + fi + if [[ ${#EXCLUDE_TAGS[@]} -gt 0 ]]; then + exclude_csv="$(join_csv "${EXCLUDE_TAGS[@]}")" + fi + printf '%s\0%s\0' --include-tags "$include_csv" + printf '%s\0%s\0' --exclude-tags "$exclude_csv" + fi +} + +common_run_args() { + printf '%s\0%s\0' --store "$STORE" + if [[ -n "$DEVICE_SELECTOR" ]]; then + printf '%s\0%s\0' --device "$DEVICE_SELECTOR" + fi + if [[ -n "$APK_PATH" ]]; then + printf '%s\0%s\0' --apk "$APK_PATH" + fi + printf '%s\0%s\0' --repeat "$REPEAT" + if [[ "$SEED" == "yes" ]]; then + printf '%s\0' --seed + else + printf '%s\0' --no-seed + fi + printf '%s\0' --no-open +} + +build_current_command() { + local args=(".maestro/scripts/run-smoke-tests.sh") + local value + while IFS= read -r -d '' value; do + args+=("$value") + done < <(selection_args) + while IFS= read -r -d '' value; do + args+=("$value") + done < <(common_run_args) + render_command "${args[@]}" +} + +build_rerun_failed_command() { + local args=(".maestro/scripts/run-smoke-tests.sh" "--rerun-failed" "$JUNIT_FILE") + local value + while IFS= read -r -d '' value; do + args+=("$value") + done < <(selection_args) + while IFS= read -r -d '' value; do + args+=("$value") + done < <(common_run_args) + render_command "${args[@]}" +} + +build_doctor_command() { + local args=(".maestro/scripts/doctor.sh") + local value + while IFS= read -r -d '' value; do + args+=("$value") + done < <(selection_args) + args+=(--store "$STORE") + if [[ -n "$DEVICE_SELECTOR" ]]; then + args+=(--device "$DEVICE_SELECTOR") + fi + if [[ "$SEED" == "yes" ]]; then + args+=(--seed) + fi + render_command "${args[@]}" +} + +RESULTS=() +PASSED=0 +FLAKY=0 +FAILED=0 +TOTAL_RUNS=$((${#ORDERED_FLOWS[@]} * REPEAT)) +SUITE_START=$(date +%s) + +echo "--- Running Maestro flows" +echo "Run ID: $SUITE_RUN_ID" +echo "Store: $STORE" +echo "Output: $OUTPUT_DIR" +echo "Repeat: $REPEAT" +echo "Include tags: ${INCLUDE_TAGS[*]:-}" +echo "Exclude tags: ${EXCLUDE_TAGS[*]:-}" +echo "Recording: $RECORD (shared-store credential paths use screenshots only)" + +run_one_attempt() { + local flow="$1" + local base="$2" + local attempt="$3" + local repeat_index="$4" + local log_file="$LOGS_DIR/${repeat_index}_${base}_attempt${attempt}.log" + local attempt_screenshots_dir="$SCREENSHOTS_DIR/${repeat_index}_${base}_attempt${attempt}" + local attempt_screenshots_rel="screenshots/${repeat_index}_${base}_attempt${attempt}/" + local media_rel="" + local device_recording="" + local host_recording="" + local screenshot_file="" + local use_video="$RECORD" + if [[ "$STORE" == "shared" ]]; then + use_video="no" + fi + if [[ "$use_video" == "yes" ]]; then + mkdir -p "$RECORDINGS_DIR" + device_recording="/sdcard/maestro_${repeat_index}_${base}_attempt${attempt}.mp4" + host_recording="$RECORDINGS_DIR/${repeat_index}_${base}_attempt${attempt}.mp4" + stop_screenrecord + collapse_system_ui + adb -s "$DEVICE_SERIAL" shell "rm -f $device_recording" >/dev/null + adb -s "$DEVICE_SERIAL" shell "screenrecord --time-limit 180 --bit-rate 4000000 $device_recording" >/dev/null 2>&1 & + RECORDER_PID=$! + sleep 1 + else + collapse_system_ui + fi + + local started ended exit_code + started=$(date +%s) + set +e + "${MAESTRO_PROCESS_ENV_ARGS[@]}" maestro test "${MAESTRO_DEVICE_ARGS[@]}" "${MAESTRO_ENV_ARGS[@]}" "$flow" >"$log_file" 2>&1 + exit_code=$? + set -e + ended=$(date +%s) + + mkdir -p "$attempt_screenshots_dir" + while IFS= read -r screenshot_name; do + [[ -z "$screenshot_name" ]] && continue + screenshot_name="${screenshot_name%.png}" + if [[ -f "$REPO_ROOT/$screenshot_name.png" ]]; then + mv "$REPO_ROOT/$screenshot_name.png" "$attempt_screenshots_dir/" + fi + done < <( + awk -F'takeScreenshot:[[:space:]]*' ' + /takeScreenshot:/ { + name = $2 + gsub(/^[[:space:]"'\''"]+|[[:space:]"'\''"]+$/, "", name) + if (name != "") print name + } + ' "$flow" + ) + if compgen -G "$attempt_screenshots_dir/*.png" >/dev/null; then + media_rel="$attempt_screenshots_rel" + else + rmdir "$attempt_screenshots_dir" 2>/dev/null || true + fi + + if [[ "$use_video" == "yes" ]]; then + stop_screenrecord + wait "$RECORDER_PID" 2>/dev/null || true + RECORDER_PID="" + if adb -s "$DEVICE_SERIAL" shell "[ -f $device_recording ]" >/dev/null 2>&1; then + adb -s "$DEVICE_SERIAL" pull "$device_recording" "$host_recording" >/dev/null 2>&1 || true + adb -s "$DEVICE_SERIAL" shell "rm -f $device_recording" >/dev/null 2>&1 || true + if [[ -f "$host_recording" ]]; then + media_rel="recordings/$(basename "$host_recording")" + fi + fi + elif [[ "$exit_code" -ne 0 ]]; then + screenshot_file="$SCREENSHOTS_DIR/${repeat_index}_${base}_attempt${attempt}.png" + device_screenshot="/sdcard/maestro_${repeat_index}_${base}_attempt${attempt}.png" + adb -s "$DEVICE_SERIAL" shell "screencap -p $device_screenshot" >/dev/null 2>&1 || true + adb -s "$DEVICE_SERIAL" pull "$device_screenshot" "$screenshot_file" >/dev/null 2>&1 || true + adb -s "$DEVICE_SERIAL" shell "rm -f $device_screenshot" >/dev/null 2>&1 || true + if [[ -f "$screenshot_file" ]]; then + media_rel="screenshots/$(basename "$screenshot_file")" + fi + fi + + scrub_log "$log_file" + local error_line recovery_count + error_line="$(grep -E '^\[Failed\]|Assertion|Couldn|Could not find|Timeout|Exception|Error' "$log_file" | head -n 1 | tr '|' '/' || true)" + recovery_count="$(grep -c '✅.*recovery_' "$log_file" || true)" + printf '%s|%s|%s|%s|%s|%s\n' "$exit_code" "$((ended - started))" "$media_rel" "logs/$(basename "$log_file")" "$error_line" "$recovery_count" +} + +run_index=0 +for repeat_index in $(seq 1 "$REPEAT"); do + for flow in "${ORDERED_FLOWS[@]}"; do + run_index=$((run_index + 1)) + base="$(basename "$flow" .yaml)" + echo "[$run_index/$TOTAL_RUNS] $base (repeat $repeat_index/$REPEAT)" + + first="$(run_one_attempt "$flow" "$base" 1 "$repeat_index")" + IFS='|' read -r first_exit first_duration first_media first_log first_error first_recovery <<< "$first" + status="PASS" + duration="$first_duration" + media="$first_media" + log_rel="$first_log" + error="$first_error" + recovery="$first_recovery" + + if [[ "$first_exit" -ne 0 ]]; then + if flow_has_any_tag "$flow" destructive; then + echo " destructive flow failed; automatic retry is disabled" + status="FAIL" + FAILED=$((FAILED + 1)) + else + echo " first attempt failed; retrying once" + retry="$(run_one_attempt "$flow" "$base" 2 "$repeat_index")" + IFS='|' read -r retry_exit retry_duration retry_media retry_log retry_error retry_recovery <<< "$retry" + duration=$((first_duration + retry_duration)) + if [[ "$retry_exit" -eq 0 ]]; then + status="FLAKY" + FLAKY=$((FLAKY + 1)) + else + status="FAIL" + FAILED=$((FAILED + 1)) + media="${retry_media:-$first_media}" + log_rel="$retry_log" + error="${retry_error:-$first_error}" + fi + recovery=$((first_recovery + retry_recovery)) + fi + elif [[ "${first_recovery:-0}" -gt 0 ]]; then + status="FLAKY_RECOVERY" + FLAKY=$((FLAKY + 1)) + else + PASSED=$((PASSED + 1)) + [[ -n "$first_log" ]] && rm -f "$OUTPUT_DIR/$first_log" 2>/dev/null || true + [[ -n "$first_media" ]] && rm -f "$OUTPUT_DIR/$first_media" 2>/dev/null || true + fi + + RESULTS+=("$status|$repeat_index|$base|$duration|$media|$log_rel|$error|$recovery") + echo " $status in ${duration}s" + done +done + +SUITE_END=$(date +%s) +SUITE_DURATION=$((SUITE_END - SUITE_START)) + +CLEANUP_FAILED=0 +if [[ "$SEED" == "yes" && "$CLEANUP" == "yes" && -f "$MANIFEST_FILE" ]]; then + echo "--- Cleaning run-owned fixtures" + CLEANUP_LOG="$LOGS_DIR/fixture-cleanup.log" + if "$SEED_SCRIPT" cleanup --manifest "$MANIFEST_FILE" --store "$STORE" >"$CLEANUP_LOG" 2>&1; then + CLEANUP_STATUS="PASS" + else + CLEANUP_STATUS="FAIL" + CLEANUP_FAILED=1 + CLEANUP_ERROR="$(tail -n 1 "$CLEANUP_LOG" | tr '|' '/' || true)" + fi + CLEANUP_DONE="yes" +elif [[ "$SEED" == "yes" ]]; then + CLEANUP_STATUS="SKIPPED" +fi +REPORT_TOTAL_RUNS=$((TOTAL_RUNS + CLEANUP_FAILED)) +REPORT_FAILURES=$((FAILED + FLAKY + CLEANUP_FAILED)) + +echo "--- Generating reports" +{ + printf '\n' + printf '\n' \ + "$REPORT_TOTAL_RUNS" "$REPORT_FAILURES" "$SUITE_DURATION" + for result in "${RESULTS[@]}"; do + IFS='|' read -r status repeat_index name duration media log_rel error recovery <<< "$result" + printf ' ' "$repeat_index" "$name" "$duration" + if [[ "$status" == "FAIL" || "$status" == "FLAKY" || "$status" == "FLAKY_RECOVERY" ]]; then + msg="$(printf '%s' "${error:-$status}" | xml_escape)" + printf '%s' "$status" "$msg" + fi + printf '\n' + done + if [[ "$CLEANUP_FAILED" -ne 0 ]]; then + cleanup_message="$(printf '%s' "${CLEANUP_ERROR:-Fixture cleanup failed}" | xml_escape)" + printf ' %s\n' "$cleanup_message" + fi + printf '\n' +} > "$JUNIT_FILE" + +CURRENT_COMMAND_HTML="$(build_current_command | xml_escape)" +RERUN_FAILED_COMMAND_HTML="$(build_rerun_failed_command | xml_escape)" +DOCTOR_COMMAND_HTML="$(build_doctor_command | xml_escape)" +OPEN_REPORT_COMMAND_HTML="$(render_command open "$REPORT_FILE" | xml_escape)" + +{ + cat < + + + +WooCommerce Android Maestro smoke report + + + +

WooCommerce Android Maestro smoke report

+

Run: $SUITE_RUN_ID | Store: $STORE | Device: $DEVICE_SERIAL | Duration: ${SUITE_DURATION}s

+

Result: $PASSED passed, $FLAKY flaky, $FAILED failed out of $TOTAL_RUNS flow executions. Cleanup: $CLEANUP_STATUS.

+
+
+

Run the same selection

+
$CURRENT_COMMAND_HTML
+
+
+

Rerun failed or flaky flows

+
$RERUN_FAILED_COMMAND_HTML
+
+
+

Pre-flight doctor

+
$DOCTOR_COMMAND_HTML
+
+
+

Open this report

+
$OPEN_REPORT_COMMAND_HTML
+
+
+ + + +HTML_HEAD + for result in "${RESULTS[@]}"; do + IFS='|' read -r status repeat_index name duration media log_rel error recovery <<< "$result" + artifact="" + if [[ -n "$media" ]]; then + artifact="media" + fi + if [[ -n "$log_rel" && -f "$OUTPUT_DIR/$log_rel" ]]; then + artifact="$artifact ${artifact:+| }log" + fi + error_html="$(printf '%s' "$error" | xml_escape)" + printf '\n' \ + "$repeat_index" "$name" "$status" "$status" "$duration" "${recovery:-0}" "$artifact" "$error_html" + done + if [[ "$CLEANUP_FAILED" -ne 0 ]]; then + error_html="$(printf '%s' "${CLEANUP_ERROR:-Fixture cleanup failed}" | xml_escape)" + printf '\n' "$error_html" + fi + cat < +
RepeatFlowStatusDurationRecoveryArtifactError
%s%s%s%ss%s%s%s
-fixture_cleanupSETUP_ERROR-0log%s
+

JUnit XML | run manifest | orphan sweep report

+ + +HTML_FOOT +} > "$REPORT_FILE" + +echo "Report: $REPORT_FILE" +echo "JUnit: $JUNIT_FILE" +echo "Result: $PASSED passed, $FLAKY flaky, $FAILED failed out of $TOTAL_RUNS flow executions; cleanup $CLEANUP_STATUS (${SUITE_DURATION}s)" + +if [[ -f "$REPORT_FILE" && "$OPEN_REPORT" == "auto" && -z "${CI:-}" && -z "${BUILDKITE:-}" && "$(uname)" == "Darwin" ]]; then + open "$REPORT_FILE" || true +fi + +if [[ "$FAILED" -gt 0 || "$FLAKY" -gt 0 || "$CLEANUP_FAILED" -gt 0 ]]; then + exit 1 +fi +exit 0 diff --git a/.maestro/scripts/seed-fixtures.py b/.maestro/scripts/seed-fixtures.py new file mode 100755 index 000000000000..f1fa9a981c27 --- /dev/null +++ b/.maestro/scripts/seed-fixtures.py @@ -0,0 +1,601 @@ +#!/usr/bin/env python3 +"""Seed and clean WooCommerce entities for Maestro smoke tests. + +The script intentionally lives outside Maestro YAML. Setup failures should stop +the suite before any flow starts, not appear as UI-test failures. +""" + +from __future__ import annotations + +import argparse +import base64 +import datetime as dt +import json +import os +import re +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path +from typing import Any + + +CONSUMABLE_MULTIPLIER = 2 +RUN_ID_RE = re.compile(r"^SUITE-\d{8,14}-[A-Za-z0-9]+$") +ORPHAN_AGE_HOURS = 48 +LOCK_TTL_SECONDS = 60 * 60 +API_PREFIX = "/wp-json/wc/v3/" + + +class SmokeSetupError(RuntimeError): + pass + + +def utc_now() -> dt.datetime: + return dt.datetime.now(dt.timezone.utc) + + +def parse_wc_date(value: str | None) -> dt.datetime | None: + if not value: + return None + normalized = value.replace("Z", "+00:00") + try: + parsed = dt.datetime.fromisoformat(normalized) + except ValueError: + return None + if parsed.tzinfo is None: + return parsed.replace(tzinfo=dt.timezone.utc) + return parsed.astimezone(dt.timezone.utc) + + +def strict_run_id(value: str) -> str: + if not RUN_ID_RE.match(value): + raise SmokeSetupError( + f"Invalid SUITE_RUN_ID {value!r}; expected SUITE--." + ) + return value + + +def env_required(name: str) -> str: + value = os.environ.get(name, "").strip() + if not value: + raise SmokeSetupError(f"Missing required environment variable: {name}") + return value + + +def load_store_env(store: str) -> None: + prefix = f"MAESTRO_WOO_{store.upper()}_" + mappings = { + "STORE_URL": ("JETPACK_STORE_URL", "STORE_URL"), + "EMAIL": ("WPCOM_EMAIL", "EMAIL"), + "PASSWORD": ("WPCOM_PASSWORD", "PASSWORD"), + "CONSUMER_KEY": ("CONSUMER_KEY",), + "CONSUMER_SECRET": ("CONSUMER_SECRET",), + } + for target, suffixes in mappings.items(): + for suffix in suffixes: + scoped = os.environ.get(prefix + suffix, "").strip() + if scoped: + os.environ[f"MAESTRO_WOO_{target}"] = scoped + break + + +class WooClient: + def __init__(self) -> None: + self.site_url = env_required("MAESTRO_WOO_STORE_URL").rstrip("/") + self.consumer_key = env_required("MAESTRO_WOO_CONSUMER_KEY") + self.consumer_secret = env_required("MAESTRO_WOO_CONSUMER_SECRET") + + def request( + self, + method: str, + path: str, + body: dict[str, Any] | None = None, + query: dict[str, Any] | None = None, + ) -> Any: + url = self.site_url + API_PREFIX + path.lstrip("/") + if query: + url += "?" + urllib.parse.urlencode(query, doseq=True) + data = None + headers = { + "Accept": "application/json", + "User-Agent": "woocommerce-android-maestro-smoke", + } + if body is not None: + data = json.dumps(body).encode("utf-8") + headers["Content-Type"] = "application/json" + token = f"{self.consumer_key}:{self.consumer_secret}".encode("utf-8") + headers["Authorization"] = "Basic " + base64.b64encode(token).decode("ascii") + request = urllib.request.Request(url, data=data, headers=headers, method=method) + try: + with urllib.request.urlopen(request, timeout=45) as response: + raw = response.read().decode("utf-8") + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") + raise SmokeSetupError(f"WooCommerce API {method} {path} failed: {exc.code} {detail}") from exc + except urllib.error.URLError as exc: + raise SmokeSetupError(f"WooCommerce API {method} {path} failed: {exc.reason}") from exc + + def create(self, path: str, payload: dict[str, Any]) -> dict[str, Any]: + return self.request("POST", path, payload) + + def delete(self, path: str, entity_id: int) -> None: + self.request("DELETE", f"{path}/{entity_id}", query={"force": "true"}) + + def list(self, path: str, **query: Any) -> list[dict[str, Any]]: + query.setdefault("per_page", 100) + return self.request("GET", path, query=query) + + +def manifest_template(run_id: str, store: str) -> dict[str, Any]: + return { + "run_id": run_id, + "store": store, + "created_at": utc_now().isoformat(), + "entities": [], + "env": {}, + "sweep_deletions": [], + "lock": None, + } + + +def record( + manifest: dict[str, Any], + entity_type: str, + entity_id: int, + label: str, + manifest_path: Path | None = None, +) -> None: + manifest["entities"].append({"type": entity_type, "id": entity_id, "label": label}) + if manifest_path is not None: + write_json(manifest_path, manifest) + + +def suite_email(run_id: str) -> str: + safe = re.sub(r"[^A-Za-z0-9]", "", run_id).lower() + return f"suite-{safe}@example.invalid" + + +def seed(args: argparse.Namespace) -> None: + run_id = strict_run_id(args.run_id) + load_store_env(args.store) + client = WooClient() + manifest_path = Path(args.manifest) + if manifest_path.exists(): + manifest = read_json(manifest_path) + manifest["run_id"] = run_id + manifest["store"] = args.store + manifest.setdefault("entities", []) + manifest.setdefault("sweep_deletions", []) + else: + manifest = manifest_template(run_id, args.store) + customer_name = f"{run_id} Tester" + product_name = f"{run_id} Variable Product" + simple_product_name = f"{run_id} Simple Product" + coupon_code = f"{run_id}-10".upper() + + tag = client.create("products/tags", {"name": f"{run_id} Tag"}) + record(manifest, "product_tag", int(tag["id"]), "variable product tag", manifest_path) + + simple_product = client.create( + "products", + { + "name": simple_product_name, + "type": "simple", + "regular_price": "12.00", + "sku": f"{run_id}-simple", + "manage_stock": True, + "stock_quantity": 20, + "tags": [{"id": tag["id"]}], + }, + ) + record(manifest, "product", int(simple_product["id"]), "simple order product", manifest_path) + + variable_product = client.create( + "products", + { + "name": product_name, + "type": "variable", + "sku": f"{run_id}-variable", + "tags": [{"id": tag["id"]}], + "attributes": [ + { + "name": "Size", + "visible": True, + "variation": True, + "options": ["Small", "Large"], + } + ], + }, + ) + record(manifest, "product", int(variable_product["id"]), "variable product", manifest_path) + for option in ("Small", "Large"): + variation = client.create( + f"products/{variable_product['id']}/variations", + { + "regular_price": "12.00", + "sku": f"{run_id}-variable-{option.lower()}", + "attributes": [{"name": "Size", "option": option}], + }, + ) + record( + manifest, + "product_variation", + int(variation["id"]), + f"variation {option}", + manifest_path, + ) + + customer = client.create( + "customers", + { + "email": suite_email(run_id), + "first_name": run_id, + "last_name": "Tester", + "username": re.sub(r"[^A-Za-z0-9]", "", run_id.lower()), + "billing": { + "first_name": run_id, + "last_name": "Tester", + "email": suite_email(run_id), + "country": "US", + }, + }, + ) + record(manifest, "customer", int(customer["id"]), "known customer", manifest_path) + + coupon = client.create( + "coupons", + { + "code": coupon_code, + "discount_type": "percent", + "amount": "10", + "description": f"{run_id} smoke coupon", + }, + ) + record(manifest, "coupon", int(coupon["id"]), "active coupon", manifest_path) + + pending_order_ids: list[int] = [] + refundable_order_ids: list[int] = [] + processing_order_ids: list[int] = [] + for index in range(1, CONSUMABLE_MULTIPLIER + 1): + pending = create_order( + client=client, + run_id=run_id, + customer=customer, + product_id=int(simple_product["id"]), + status="pending", + set_paid=False, + label=f"pending-order-{index}", + ) + pending_order_ids.append(int(pending["id"])) + record(manifest, "order", int(pending["id"]), f"pending-order-{index}", manifest_path) + + refundable = create_order( + client=client, + run_id=run_id, + customer=customer, + product_id=int(simple_product["id"]), + status="completed", + set_paid=True, + label=f"refundable-order-{index}", + ) + refundable_order_ids.append(int(refundable["id"])) + record( + manifest, + "order", + int(refundable["id"]), + f"refundable-order-{index}", + manifest_path, + ) + + # Paid order awaiting fulfillment — the only status where the + # order detail screen exposes the Mark Complete action. + processing = create_order( + client=client, + run_id=run_id, + customer=customer, + product_id=int(simple_product["id"]), + status="processing", + set_paid=True, + label=f"processing-order-{index}", + ) + processing_order_ids.append(int(processing["id"])) + record( + manifest, + "order", + int(processing["id"]), + f"processing-order-{index}", + manifest_path, + ) + + manifest["env"] = { + "MAESTRO_SUITE_RUN_ID": run_id, + "MAESTRO_FIXTURE_CUSTOMER_NAME": customer_name, + "MAESTRO_FIXTURE_CUSTOMER_EMAIL": suite_email(run_id), + "MAESTRO_FIXTURE_VARIABLE_PRODUCT": product_name, + "MAESTRO_FIXTURE_SIMPLE_PRODUCT": simple_product_name, + "MAESTRO_FIXTURE_COUPON_CODE": coupon_code, + "MAESTRO_FIXTURE_PENDING_ORDER_ID": str(pending_order_ids[0]), + "MAESTRO_FIXTURE_PENDING_ORDER_IDS": ",".join(str(item) for item in pending_order_ids), + "MAESTRO_FIXTURE_REFUNDABLE_ORDER_ID": str(refundable_order_ids[0]), + "MAESTRO_FIXTURE_REFUNDABLE_ORDER_IDS": ",".join(str(item) for item in refundable_order_ids), + "MAESTRO_FIXTURE_PROCESSING_ORDER_ID": str(processing_order_ids[0]), + "MAESTRO_FIXTURE_PROCESSING_ORDER_IDS": ",".join(str(item) for item in processing_order_ids), + } + + write_json(args.manifest, manifest) + if args.env_file: + write_env_file(Path(args.env_file), manifest["env"]) + print(f"Seeded {len(manifest['entities'])} entities for {run_id}") + + +def create_order( + client: WooClient, + run_id: str, + customer: dict[str, Any], + product_id: int, + status: str, + set_paid: bool, + label: str, +) -> dict[str, Any]: + return client.create( + "orders", + { + "status": status, + "set_paid": set_paid, + "customer_id": customer["id"], + "payment_method": "cod", + "payment_method_title": "Cash on delivery", + "billing": { + "first_name": run_id, + "last_name": "Tester", + "email": customer["email"], + "country": "US", + }, + "shipping": { + "first_name": run_id, + "last_name": "Tester", + "country": "US", + }, + "customer_note": f"{run_id} {label}", + "line_items": [{"product_id": product_id, "quantity": 1}], + "meta_data": [{"key": "suite_run_id", "value": run_id}], + }, + ) + + +def cleanup(args: argparse.Namespace) -> None: + manifest_path = Path(args.manifest) + manifest = read_json(manifest_path) + load_store_env(manifest.get("store", args.store or "lab")) + client = WooClient() + errors: list[str] = [] + original_count = len(manifest.get("entities", [])) + type_to_path = { + "order": "orders", + "coupon": "coupons", + "product_variation": None, + "product": "products", + "product_tag": "products/tags", + "customer": "customers", + "lock_product": "products", + } + for entity in reversed(list(manifest.get("entities", []))): + entity_type = entity.get("type") + path = type_to_path.get(entity_type) + if path is None: + manifest["entities"].remove(entity) + write_json(manifest_path, manifest) + continue + try: + client.delete(path, int(entity["id"])) + except SmokeSetupError as exc: + errors.append(str(exc)) + else: + manifest["entities"].remove(entity) + write_json(manifest_path, manifest) + if errors: + for error in errors: + print(error, file=sys.stderr) + raise SmokeSetupError(f"Cleanup completed with {len(errors)} deletion error(s).") + print(f"Cleaned {original_count} manifest entities") + + +def sweep(args: argparse.Namespace) -> None: + load_store_env(args.store) + client = WooClient() + deleted: list[dict[str, Any]] = [] + candidates: list[tuple[str, str, dict[str, Any]]] = [] + for entity_type, path in ( + ("product", "products"), + ("coupon", "coupons"), + ("order", "orders"), + ("customer", "customers"), + ): + query: dict[str, Any] = {"search": "SUITE-"} + if entity_type in {"product", "order"}: + query["status"] = "any" + for item in client.list(path, **query): + candidates.append((entity_type, path, item)) + + for entity_type, path, item in candidates: + label = entity_label(entity_type, item) + match = re.search(r"SUITE-\d{8,14}-[A-Za-z0-9]+", label) + if "SUITE-" in label and not match: + raise SmokeSetupError( + f"Orphan sweep refused loose automation match for {entity_type} {item.get('id')}: {label!r}" + ) + if not match: + continue + created = parse_wc_date(item.get("date_created_gmt") or item.get("date_created")) + if created is None: + continue + age = utc_now() - created + if age < dt.timedelta(hours=ORPHAN_AGE_HOURS): + continue + record_item = { + "type": entity_type, + "id": item.get("id"), + "label": label, + "age_hours": round(age.total_seconds() / 3600, 1), + "dry_run": args.dry_run, + } + deleted.append(record_item) + if not args.dry_run: + client.delete(path, int(item["id"])) + + if args.report: + write_json(Path(args.report), {"deleted": deleted, "dry_run": args.dry_run}) + action = "Would delete" if args.dry_run else "Deleted" + print(f"{action} {len(deleted)} stale automation orphan(s)") + + +def lock(args: argparse.Namespace) -> None: + run_id = strict_run_id(args.run_id) + load_store_env(args.store) + client = WooClient() + locks = client.list("products", search="SUITE-LOCK-", status="any") + now = utc_now() + for item in locks: + name = str(item.get("name", "")) + if not name.startswith("SUITE-LOCK-"): + continue + created = parse_wc_date(item.get("date_created_gmt") or item.get("date_created")) + expired = created is None or (now - created).total_seconds() > args.ttl_seconds + if expired: + print(f"Deleting expired shared-store lock product {item.get('id')}: {name}") + client.delete("products", int(item["id"])) + continue + raise SmokeSetupError(f"Shared store is locked by {name} (product {item.get('id')}).") + + product = client.create( + "products", + { + "name": f"SUITE-LOCK-{run_id}-{int(time.time())}", + "type": "simple", + "status": "draft", + "catalog_visibility": "hidden", + "regular_price": "0", + "sku": f"lock-{run_id}", + }, + ) + lock_record = {"type": "lock_product", "id": int(product["id"]), "label": product["name"]} + if args.manifest: + path = Path(args.manifest) + manifest = read_json(path) if path.exists() else manifest_template(run_id, args.store) + manifest["lock"] = lock_record + manifest.setdefault("entities", []) + write_json(path, manifest) + print(json.dumps(lock_record)) + + +def unlock(args: argparse.Namespace) -> None: + load_store_env(args.store) + client = WooClient() + lock_id = args.lock_id + if not lock_id and args.manifest and Path(args.manifest).exists(): + manifest = read_json(Path(args.manifest)) + lock_data = manifest.get("lock") or {} + lock_id = lock_data.get("id") + if not lock_id: + print("No lock id supplied; nothing to unlock") + return + client.delete("products", int(lock_id)) + print(f"Deleted shared-store lock product {lock_id}") + + +def entity_label(entity_type: str, item: dict[str, Any]) -> str: + if entity_type == "coupon": + return str(item.get("code", "")) + if entity_type == "customer": + return " ".join( + str(item.get(key, "")) for key in ("first_name", "last_name", "email", "username") + ) + if entity_type == "order": + billing = item.get("billing", {}) or {} + return " ".join( + str(value) + for value in ( + item.get("customer_note", ""), + billing.get("first_name", ""), + billing.get("last_name", ""), + billing.get("email", ""), + ) + ) + return str(item.get("name", "")) + + +def write_env_file(path: Path, values: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as handle: + for key, value in sorted(values.items()): + escaped = value.replace("\\", "\\\\").replace('"', '\\"') + handle.write(f'export {key}="{escaped}"\n') + + +def write_json(path_value: str | Path, value: dict[str, Any]) -> None: + path = Path(path_value) + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_suffix(path.suffix + ".tmp") + temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8") + temporary.replace(path) + + +def read_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + + seed_parser = subparsers.add_parser("seed") + seed_parser.add_argument("--store", choices=("lab", "shared"), required=True) + seed_parser.add_argument("--run-id", required=True) + seed_parser.add_argument("--manifest", required=True) + seed_parser.add_argument("--env-file") + seed_parser.set_defaults(func=seed) + + cleanup_parser = subparsers.add_parser("cleanup") + cleanup_parser.add_argument("--manifest", required=True) + cleanup_parser.add_argument("--store", choices=("lab", "shared")) + cleanup_parser.set_defaults(func=cleanup) + + sweep_parser = subparsers.add_parser("sweep") + sweep_parser.add_argument("--store", choices=("lab", "shared"), required=True) + sweep_parser.add_argument("--report") + sweep_parser.add_argument("--dry-run", action="store_true") + sweep_parser.set_defaults(func=sweep) + + lock_parser = subparsers.add_parser("lock") + lock_parser.add_argument("--store", choices=("shared",), default="shared") + lock_parser.add_argument("--run-id", required=True) + lock_parser.add_argument("--manifest") + lock_parser.add_argument("--ttl-seconds", type=int, default=LOCK_TTL_SECONDS) + lock_parser.set_defaults(func=lock) + + unlock_parser = subparsers.add_parser("unlock") + unlock_parser.add_argument("--store", choices=("shared",), default="shared") + unlock_parser.add_argument("--manifest") + unlock_parser.add_argument("--lock-id", type=int) + unlock_parser.set_defaults(func=unlock) + + return parser + + +def main() -> int: + parser = build_parser() + args = parser.parse_args() + try: + args.func(args) + return 0 + except SmokeSetupError as exc: + print(f"Setup error: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.maestro/scripts/smoke_plan.py b/.maestro/scripts/smoke_plan.py new file mode 100755 index 000000000000..abcbc9d6ac20 --- /dev/null +++ b/.maestro/scripts/smoke_plan.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +"""Canonical profile and flow-selection policy for Android Maestro smoke tests.""" + +from __future__ import annotations + +import argparse +import sys +from dataclasses import dataclass +from pathlib import Path + + +SCRIPT_DIR = Path(__file__).resolve().parent +REPO_ROOT = SCRIPT_DIR.parent.parent +FLOWS_DIR = REPO_ROOT / ".maestro" / "flows" + + +@dataclass(frozen=True) +class Profile: + store: str + include: tuple[str, ...] + exclude: tuple[str, ...] + repeat: int = 1 + + +PROFILES = { + "core": Profile("lab", ("smoke_core",), ("flaky_quarantine", "android_system")), + "phone-full": Profile( + "lab", + ("smoke_core", "smoke_extended"), + ("pos_tablet", "android_system"), + ), + "release": Profile( + "shared", + ("smoke_core", "smoke_extended", "destructive"), + ("flaky_quarantine", "pos_tablet", "android_system"), + ), + "burst": Profile( + "shared", + ("smoke_core", "smoke_extended", "destructive"), + ("flaky_quarantine", "pos_tablet", "android_system"), + repeat=3, + ), + "pos-tablet": Profile("lab", ("pos_tablet",), ()), + "android-system": Profile("lab", ("android_system",), ()), +} + +P2_ORDERED_FLOW_NAMES = ( + "login_not_wp_site.yaml", + "login_wrong_credentials.yaml", + "login_help.yaml", + "login_not_woo_store.yaml", + "login_wrong_account.yaml", + "login_no_jetpack.yaml", + "login_google.yaml", + "login_successful.yaml", + "dashboard_stats.yaml", + "dashboard_view_all_analytics.yaml", + "dashboard_customize.yaml", + "orders_list_and_search.yaml", + "orders_create.yaml", + "orders_details_and_actions.yaml", + "orders_mark_complete.yaml", + "orders_cash_payment.yaml", + "orders_barcode_scanner_opens.yaml", + "orders_payment_qr_and_share.yaml", + "orders_refund.yaml", + "products_list_and_sort.yaml", + "products_detail.yaml", + "products_variations_and_tags.yaml", + "products_create.yaml", + "products_media_upload.yaml", + "hub_menu_settings.yaml", + "hub_menu_payments.yaml", + "hub_menu_coupons.yaml", + "hub_menu_customers_inbox.yaml", + "hub_menu_admin_and_store.yaml", + "blaze_campaign.yaml", + "google_for_woo.yaml", + "pos_search_and_coupons.yaml", + "pos_cash_payment.yaml", + "android_quick_actions.yaml", +) + + +def parse_csv(value: str) -> tuple[str, ...]: + return tuple(item.strip() for item in value.split(",") if item.strip()) + + +def flow_tags(path: Path) -> frozenset[str]: + tags: set[str] = set() + in_tags = False + header = path.read_text(errors="replace").split("---", 1)[0] + for line in header.splitlines(): + if line.strip() == "tags:": + in_tags = True + continue + if in_tags and line.lstrip().startswith("-"): + tags.add(line.split("-", 1)[1].strip()) + elif in_tags and line and not line.startswith((" ", "\t")): + in_tags = False + return frozenset(tags) + + +def selected_flows( + include_tags: tuple[str, ...], + exclude_tags: tuple[str, ...], + flows_dir: Path = FLOWS_DIR, +) -> tuple[Path, ...]: + selected: list[Path] = [] + for name in P2_ORDERED_FLOW_NAMES: + path = flows_dir / name + if not path.exists(): + continue + tags = flow_tags(path) + if include_tags and tags.isdisjoint(include_tags): + continue + if exclude_tags and not tags.isdisjoint(exclude_tags): + continue + selected.append(path) + return tuple(selected) + + +def print_profile(name: str) -> None: + profile = PROFILES[name] + print(f"store\t{profile.store}") + print(f"repeat\t{profile.repeat}") + print(f"include\t{','.join(profile.include)}") + print(f"exclude\t{','.join(profile.exclude)}") + + +def print_plan(args: argparse.Namespace) -> int: + include_tags = parse_csv(args.include_tags) + exclude_tags = parse_csv(args.exclude_tags) + flows = selected_flows(include_tags, exclude_tags) + if not flows: + print("No flows matched the current filters.", file=sys.stderr) + return 1 + + print("Maestro smoke plan") + print(f" profile: {args.profile_label or ''}") + print(f" store: {args.store}") + print(f" repeat: {args.repeat}") + print(f" include: {','.join(include_tags) or ''}") + print(f" exclude: {','.join(exclude_tags) or ''}") + print(f" seed: {'yes' if args.seed else 'no'}") + print(f" flows: {len(flows)}") + print() + print("Selected flows:") + for flow in flows: + print(f" - {flow.relative_to(REPO_ROOT)}") + return 0 + + +def print_selection(args: argparse.Namespace) -> int: + flows = selected_flows(parse_csv(args.include_tags), parse_csv(args.exclude_tags)) + if not flows: + print("No flows matched the current filters.", file=sys.stderr) + return 1 + for flow in flows: + print(flow) + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest="command", required=True) + + profile_parser = subparsers.add_parser("profile") + profile_parser.add_argument("name", choices=sorted(PROFILES)) + + plan_parser = subparsers.add_parser("plan") + plan_parser.add_argument("--profile-label", default="") + plan_parser.add_argument("--store", choices=("lab", "shared"), required=True) + plan_parser.add_argument("--repeat", type=int, required=True) + plan_parser.add_argument("--include-tags", default="") + plan_parser.add_argument("--exclude-tags", default="") + plan_parser.add_argument("--seed", action="store_true") + + select_parser = subparsers.add_parser("select") + select_parser.add_argument("--include-tags", default="") + select_parser.add_argument("--exclude-tags", default="") + + args = parser.parse_args() + if args.command == "profile": + print_profile(args.name) + return 0 + if args.command == "select": + return print_selection(args) + return print_plan(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.maestro/scripts/tests/golden/burst-plan.txt b/.maestro/scripts/tests/golden/burst-plan.txt new file mode 100644 index 000000000000..e2ddb3cc1453 --- /dev/null +++ b/.maestro/scripts/tests/golden/burst-plan.txt @@ -0,0 +1,14 @@ +Maestro smoke plan + profile: burst + store: shared + repeat: 3 + include: smoke_core,smoke_extended,destructive + exclude: flaky_quarantine,pos_tablet,android_system + seed: no + flows: 4 + +Selected flows: + - .maestro/flows/login_successful.yaml + - .maestro/flows/dashboard_stats.yaml + - .maestro/flows/orders_list_and_search.yaml + - .maestro/flows/products_list_and_sort.yaml diff --git a/.maestro/scripts/tests/golden/core-plan.txt b/.maestro/scripts/tests/golden/core-plan.txt new file mode 100644 index 000000000000..542e03058b8e --- /dev/null +++ b/.maestro/scripts/tests/golden/core-plan.txt @@ -0,0 +1,14 @@ +Maestro smoke plan + profile: core + store: lab + repeat: 1 + include: smoke_core + exclude: flaky_quarantine,android_system + seed: no + flows: 4 + +Selected flows: + - .maestro/flows/login_successful.yaml + - .maestro/flows/dashboard_stats.yaml + - .maestro/flows/orders_list_and_search.yaml + - .maestro/flows/products_list_and_sort.yaml diff --git a/.maestro/scripts/tests/golden/phone-full-plan.txt b/.maestro/scripts/tests/golden/phone-full-plan.txt new file mode 100644 index 000000000000..3f27696b5825 --- /dev/null +++ b/.maestro/scripts/tests/golden/phone-full-plan.txt @@ -0,0 +1,41 @@ +Maestro smoke plan + profile: phone-full + store: lab + repeat: 1 + include: smoke_core,smoke_extended + exclude: pos_tablet,android_system + seed: no + flows: 31 + +Selected flows: + - .maestro/flows/login_not_wp_site.yaml + - .maestro/flows/login_wrong_credentials.yaml + - .maestro/flows/login_help.yaml + - .maestro/flows/login_not_woo_store.yaml + - .maestro/flows/login_wrong_account.yaml + - .maestro/flows/login_no_jetpack.yaml + - .maestro/flows/login_google.yaml + - .maestro/flows/login_successful.yaml + - .maestro/flows/dashboard_stats.yaml + - .maestro/flows/dashboard_view_all_analytics.yaml + - .maestro/flows/dashboard_customize.yaml + - .maestro/flows/orders_list_and_search.yaml + - .maestro/flows/orders_create.yaml + - .maestro/flows/orders_details_and_actions.yaml + - .maestro/flows/orders_mark_complete.yaml + - .maestro/flows/orders_cash_payment.yaml + - .maestro/flows/orders_barcode_scanner_opens.yaml + - .maestro/flows/orders_payment_qr_and_share.yaml + - .maestro/flows/orders_refund.yaml + - .maestro/flows/products_list_and_sort.yaml + - .maestro/flows/products_detail.yaml + - .maestro/flows/products_variations_and_tags.yaml + - .maestro/flows/products_create.yaml + - .maestro/flows/products_media_upload.yaml + - .maestro/flows/hub_menu_settings.yaml + - .maestro/flows/hub_menu_payments.yaml + - .maestro/flows/hub_menu_coupons.yaml + - .maestro/flows/hub_menu_customers_inbox.yaml + - .maestro/flows/hub_menu_admin_and_store.yaml + - .maestro/flows/blaze_campaign.yaml + - .maestro/flows/google_for_woo.yaml diff --git a/.maestro/scripts/tests/golden/release-plan.txt b/.maestro/scripts/tests/golden/release-plan.txt new file mode 100644 index 000000000000..3bc9c75c031e --- /dev/null +++ b/.maestro/scripts/tests/golden/release-plan.txt @@ -0,0 +1,14 @@ +Maestro smoke plan + profile: release + store: shared + repeat: 1 + include: smoke_core,smoke_extended,destructive + exclude: flaky_quarantine,pos_tablet,android_system + seed: no + flows: 4 + +Selected flows: + - .maestro/flows/login_successful.yaml + - .maestro/flows/dashboard_stats.yaml + - .maestro/flows/orders_list_and_search.yaml + - .maestro/flows/products_list_and_sort.yaml diff --git a/.maestro/scripts/tests/golden/smoke-extended-plan.txt b/.maestro/scripts/tests/golden/smoke-extended-plan.txt new file mode 100644 index 000000000000..f08bbc74ffeb --- /dev/null +++ b/.maestro/scripts/tests/golden/smoke-extended-plan.txt @@ -0,0 +1,37 @@ +Maestro smoke plan + profile: + store: lab + repeat: 1 + include: smoke_extended + exclude: + seed: no + flows: 27 + +Selected flows: + - .maestro/flows/login_not_wp_site.yaml + - .maestro/flows/login_wrong_credentials.yaml + - .maestro/flows/login_help.yaml + - .maestro/flows/login_not_woo_store.yaml + - .maestro/flows/login_wrong_account.yaml + - .maestro/flows/login_no_jetpack.yaml + - .maestro/flows/login_google.yaml + - .maestro/flows/dashboard_view_all_analytics.yaml + - .maestro/flows/dashboard_customize.yaml + - .maestro/flows/orders_create.yaml + - .maestro/flows/orders_details_and_actions.yaml + - .maestro/flows/orders_mark_complete.yaml + - .maestro/flows/orders_cash_payment.yaml + - .maestro/flows/orders_barcode_scanner_opens.yaml + - .maestro/flows/orders_payment_qr_and_share.yaml + - .maestro/flows/orders_refund.yaml + - .maestro/flows/products_detail.yaml + - .maestro/flows/products_variations_and_tags.yaml + - .maestro/flows/products_create.yaml + - .maestro/flows/products_media_upload.yaml + - .maestro/flows/hub_menu_settings.yaml + - .maestro/flows/hub_menu_payments.yaml + - .maestro/flows/hub_menu_coupons.yaml + - .maestro/flows/hub_menu_customers_inbox.yaml + - .maestro/flows/hub_menu_admin_and_store.yaml + - .maestro/flows/blaze_campaign.yaml + - .maestro/flows/google_for_woo.yaml diff --git a/.maestro/scripts/tests/test_annotate_run.py b/.maestro/scripts/tests/test_annotate_run.py new file mode 100644 index 000000000000..e4d55bca3d3c --- /dev/null +++ b/.maestro/scripts/tests/test_annotate_run.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import importlib.util +import json +import sys +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).resolve().parents[1] / "annotate-run.py" +SPEC = importlib.util.spec_from_file_location("annotate_run", SCRIPT) +assert SPEC and SPEC.loader +ANNOTATE = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = ANNOTATE +SPEC.loader.exec_module(ANNOTATE) + + +class AnnotateRunTests(unittest.TestCase): + def test_summary_status_and_failed_cases_are_failure_first(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + junit = root / "report.xml" + summary = root / "run-summary.json" + junit.write_text( + '' + '' + '', + encoding="utf-8", + ) + summary.write_text(json.dumps({"status": "FLAKY"}), encoding="utf-8") + + rendered = ANNOTATE.render(junit, summary) + + self.assertTrue(rendered.startswith("### Maestro smoke: FLAKY")) + self.assertIn("2 tests · 1 failures · 0 skipped", rendered) + self.assertIn("- `flakes`", rendered) + + +if __name__ == "__main__": + unittest.main() diff --git a/.maestro/scripts/tests/test_check_smoke_coverage.py b/.maestro/scripts/tests/test_check_smoke_coverage.py new file mode 100644 index 000000000000..4faafdcd3789 --- /dev/null +++ b/.maestro/scripts/tests/test_check_smoke_coverage.py @@ -0,0 +1,94 @@ +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).parents[1] / "check-smoke-coverage.py" + + +class CheckSmokeCoverageTests(unittest.TestCase): + def test_rejects_item_declared_by_a_different_flow_than_its_mapping(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + flows = root / "flows" + flows.mkdir() + (flows / "mapped.yaml").write_text("# p2: other.item\n", encoding="utf-8") + (flows / "claiming.yaml").write_text("# p2: target.item\n", encoding="utf-8") + coverage = root / "coverage.yaml" + coverage.write_text( + "items:\n" + " - id: target.item\n" + f" flow: {flows / 'mapped.yaml'}\n" + " - id: other.item\n" + f" flow: {flows / 'mapped.yaml'}\n", + encoding="utf-8", + ) + + result = subprocess.run( + [sys.executable, str(SCRIPT), "--coverage", str(coverage), "--flows-dir", str(flows)], + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(1, result.returncode) + self.assertIn("target.item", result.stderr) + self.assertIn("mapped.yaml", result.stderr) + + def test_rejects_duplicate_item_ids(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + flows = root / "flows" + flows.mkdir() + (flows / "flow.yaml").write_text("# p2: duplicate.item\n", encoding="utf-8") + coverage = root / "coverage.yaml" + coverage.write_text( + "items:\n" + " - id: duplicate.item\n" + f" flow: {flows / 'flow.yaml'}\n" + " - id: duplicate.item\n" + f" flow: {flows / 'flow.yaml'}\n", + encoding="utf-8", + ) + + result = subprocess.run( + [sys.executable, str(SCRIPT), "--coverage", str(coverage), "--flows-dir", str(flows)], + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(1, result.returncode) + self.assertIn("duplicate item id duplicate.item", result.stderr) + + def test_rejects_an_item_that_is_both_automated_and_manual(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + flows = root / "flows" + flows.mkdir() + flow = flows / "flow.yaml" + flow.write_text("# p2: ambiguous.item\n", encoding="utf-8") + coverage = root / "coverage.yaml" + coverage.write_text( + "items:\n" + " - id: ambiguous.item\n" + f" flow: {flow}\n" + " manual: This must not be accepted as both states.\n", + encoding="utf-8", + ) + + result = subprocess.run( + [sys.executable, str(SCRIPT), "--coverage", str(coverage), "--flows-dir", str(flows)], + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(1, result.returncode) + self.assertIn("ambiguous.item has both flow and manual reason", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/.maestro/scripts/tests/test_check_toolchain.py b/.maestro/scripts/tests/test_check_toolchain.py new file mode 100644 index 000000000000..b2fb3d092d93 --- /dev/null +++ b/.maestro/scripts/tests/test_check_toolchain.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).resolve().parents[1] / "check-toolchain.py" +CONFIGURE = Path(__file__).resolve().parents[1] / "configure-toolchain.sh" +REPO_ROOT = Path(__file__).resolve().parents[3] + + +class CheckToolchainTests(unittest.TestCase): + def test_reports_matching_maestro_and_java_versions(self) -> None: + result = self.run_checker(maestro_version="2.8.0", java_version="21.0.8") + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("Maestro: expected 2.8.0, actual 2.8.0", result.stdout) + self.assertIn("Java: expected major 21, actual 21.0.8", result.stdout) + self.assertIn("Maestro toolchain OK", result.stdout) + + def test_fails_clearly_when_maestro_version_does_not_match(self) -> None: + result = self.run_checker(maestro_version="2.7.0", java_version="21.0.8") + + self.assertEqual(1, result.returncode) + self.assertIn("Maestro version mismatch: expected 2.8.0, actual 2.7.0", result.stderr) + + def test_fails_clearly_when_java_major_version_does_not_match(self) -> None: + result = self.run_checker(maestro_version="2.8.0", java_version="17.0.12") + + self.assertEqual(1, result.returncode) + self.assertIn("Java version mismatch: expected major 21, actual 17.0.12", result.stderr) + + def test_fails_clearly_when_maestro_is_missing(self) -> None: + result = self.run_checker(maestro_version=None, java_version="21.0.8") + + self.assertEqual(2, result.returncode) + self.assertIn("Required command not found: maestro", result.stderr) + + def test_fails_clearly_when_maestro_version_output_is_unrecognized(self) -> None: + result = self.run_checker(maestro_version="Maestro dev build", java_version="21.0.8") + + self.assertEqual(2, result.returncode) + self.assertIn("Could not parse Maestro version output", result.stderr) + + def test_ci_configuration_accepts_an_already_matching_toolchain_without_installing(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + bin_dir = root / "bin" + bin_dir.mkdir() + self.write_executable(bin_dir / "maestro", "printf '%s\\n' '2.8.0'\n") + self.write_executable( + bin_dir / "java", + "printf '%s\\n' 'openjdk version \"21.0.8\"' >&2\n", + ) + environment = dict(os.environ) + environment.update( + { + "HOME": str(root), + "PATH": f"{bin_dir}:/usr/bin:/bin", + } + ) + + result = subprocess.run( + ["/bin/bash", "-c", 'source "$1"', "bash", str(CONFIGURE)], + cwd=REPO_ROOT, + env=environment, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertNotIn("Installing pinned Maestro", result.stdout) + + def test_ci_configuration_installs_a_verified_release_into_the_job_workspace(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + bin_dir = root / "bin" + bin_dir.mkdir() + curl_log = root / "curl.log" + archive_maestro = root / "archive-maestro" + self.write_executable(archive_maestro, "printf '%s\\n' '2.8.0'\n") + self.write_executable(bin_dir / "maestro", "printf '%s\\n' '2.7.0'\n") + self.write_executable( + bin_dir / "java", + "printf '%s\\n' 'openjdk version \"21.0.8\"' >&2\n", + ) + self.write_executable( + bin_dir / "curl", + f"printf '%s\\n' \"$*\" > '{curl_log}'\n" + "destination=''\n" + "while [ \"$#\" -gt 0 ]; do\n" + " if [ \"$1\" = '-o' ]; then destination=\"$2\"; break; fi\n" + " shift\n" + "done\n" + "printf '%s\\n' archive > \"$destination\"\n", + ) + self.write_executable( + bin_dir / "sha256sum", + "printf '%s %s\\n' " + "'b3e561161904fb391875ca5834d5b22cf0b01c052dd1b408ad83e30d8f8951b3' \"$1\"\n", + ) + self.write_executable( + bin_dir / "unzip", + "destination=''\n" + "while [ \"$#\" -gt 0 ]; do\n" + " if [ \"$1\" = '-d' ]; then destination=\"$2\"; break; fi\n" + " shift\n" + "done\n" + "mkdir -p \"$destination/maestro/bin\"\n" + "cp \"$FAKE_MAESTRO_BIN\" \"$destination/maestro/bin/maestro\"\n", + ) + environment = dict(os.environ) + environment.update( + { + "HOME": str(root), + "FAKE_MAESTRO_BIN": str(archive_maestro), + "MAESTRO_TOOLCHAIN_ROOT": str(root / "toolchain"), + "PATH": f"{bin_dir}:/usr/bin:/bin", + } + ) + + result = subprocess.run( + ["/bin/bash", "-c", 'source "$1"', "bash", str(CONFIGURE)], + cwd=REPO_ROOT, + env=environment, + capture_output=True, + text=True, + check=False, + ) + installed = list((root / "toolchain").glob("maestro-2.8.0-*/bin/maestro")) + requested_url = curl_log.read_text(encoding="utf-8") + + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual(1, len(installed)) + self.assertIn( + "https://github.com/mobile-dev-inc/Maestro/releases/download/cli-2.8.0/maestro.zip", + requested_url, + ) + self.assertIn("Installing verified Maestro 2.8.0", result.stdout) + + def run_checker( + self, + *, + maestro_version: str | None, + java_version: str, + ) -> subprocess.CompletedProcess[str]: + with tempfile.TemporaryDirectory() as directory: + bin_dir = Path(directory) + if maestro_version is not None: + self.write_executable( + bin_dir / "maestro", + f'printf \'%s\\n\' \'{maestro_version}\'\n', + ) + self.write_executable( + bin_dir / "java", + f'printf \'%s\\n\' \'openjdk version "{java_version}"\' >&2\n', + ) + environment = dict(os.environ) + environment["PATH"] = str(bin_dir) + return subprocess.run( + [sys.executable, str(SCRIPT)], + cwd=REPO_ROOT, + env=environment, + capture_output=True, + text=True, + check=False, + ) + + @staticmethod + def write_executable(path: Path, command: str) -> None: + path.write_text(f"#!/bin/sh\n{command}", encoding="utf-8") + path.chmod(0o755) + + +if __name__ == "__main__": + unittest.main() diff --git a/.maestro/scripts/tests/test_ci_contract.py b/.maestro/scripts/tests/test_ci_contract.py new file mode 100644 index 000000000000..8c5ca62ccdf5 --- /dev/null +++ b/.maestro/scripts/tests/test_ci_contract.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +class MaestroCiContractTests(unittest.TestCase): + def test_toolchain_is_configured_before_building_the_app(self) -> None: + wrapper = ( + REPO_ROOT / ".buildkite" / "commands" / "run-maestro-tests.sh" + ).read_text(encoding="utf-8") + + self.assertLess( + wrapper.index("source .maestro/scripts/configure-toolchain.sh"), + wrapper.index("./gradlew :WooCommerce:installWasabiDebug"), + ) + + def test_changed_file_skip_only_applies_to_pull_requests(self) -> None: + wrapper = ( + REPO_ROOT / ".buildkite" / "commands" / "run-maestro-tests.sh" + ).read_text(encoding="utf-8") + + self.assertIn( + 'if [[ "${BUILDKITE_PULL_REQUEST:-false}" != "false" ]] &&', + wrapper, + ) + self.assertIn( + ".buildkite/commands/should-skip-job.sh --job-type validation; then", + wrapper, + ) + + def test_shared_store_steps_are_serialized(self) -> None: + pipeline_files = [ + REPO_ROOT / ".buildkite" / "pipeline.yml", + REPO_ROOT / ".buildkite" / "schedules" / "maestro-smoke-burst.yml", + REPO_ROOT / ".buildkite" / "release-pipelines" / "maestro-smoke.yml", + ] + + for path in pipeline_files: + with self.subTest(path=path): + text = path.read_text(encoding="utf-8") + self.assertIn('concurrency_group: "woocommerce-android/maestro/shared-store"', text) + self.assertIn("concurrency: 1", text) + + def test_shared_destructive_ci_runs_seed_owned_fixtures(self) -> None: + pipeline_files = [ + REPO_ROOT / ".buildkite" / "schedules" / "maestro-smoke-burst.yml", + REPO_ROOT / ".buildkite" / "release-pipelines" / "maestro-smoke.yml", + ] + + for path in pipeline_files: + with self.subTest(path=path): + text = path.read_text(encoding="utf-8") + self.assertIn('MAESTRO_SEED: "true"', text) + + +if __name__ == "__main__": + unittest.main() diff --git a/.maestro/scripts/tests/test_seed_fixtures.py b/.maestro/scripts/tests/test_seed_fixtures.py new file mode 100644 index 000000000000..60deed226bf2 --- /dev/null +++ b/.maestro/scripts/tests/test_seed_fixtures.py @@ -0,0 +1,131 @@ +import argparse +import contextlib +import importlib.util +import io +import json +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).parents[1] / "seed-fixtures.py" +SPEC = importlib.util.spec_from_file_location("seed_fixtures", SCRIPT) +assert SPEC and SPEC.loader +seed_fixtures = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(seed_fixtures) + + +class FailingWooClient: + def __init__(self) -> None: + self.create_count = 0 + + def create(self, path: str, payload: dict) -> dict: + self.create_count += 1 + if self.create_count == 2: + raise seed_fixtures.SmokeSetupError("injected create failure") + return {"id": 101} + + +class LockWooClient: + def list(self, path: str, **query) -> list[dict]: + return [] + + def create(self, path: str, payload: dict) -> dict: + return {"id": 202, "name": payload["name"]} + + +class PartiallyFailingCleanupClient: + def __init__(self) -> None: + self.delete_count = 0 + + def delete(self, path: str, entity_id: int) -> None: + self.delete_count += 1 + if self.delete_count == 2: + raise seed_fixtures.SmokeSetupError("injected cleanup failure") + + +class SeedFixturesTests(unittest.TestCase): + def test_failed_seed_persists_every_entity_created_before_the_failure(self) -> None: + with tempfile.TemporaryDirectory() as directory: + manifest = Path(directory) / "run-manifest.json" + args = argparse.Namespace( + run_id="SUITE-20260805-abc123", + store="lab", + manifest=str(manifest), + env_file=None, + ) + original_client = seed_fixtures.WooClient + seed_fixtures.WooClient = FailingWooClient + try: + with self.assertRaisesRegex(seed_fixtures.SmokeSetupError, "injected create failure"): + seed_fixtures.seed(args) + finally: + seed_fixtures.WooClient = original_client + + saved = json.loads(manifest.read_text(encoding="utf-8")) + + self.assertEqual( + [{"id": 101, "label": "variable product tag", "type": "product_tag"}], + saved["entities"], + ) + + def test_lock_lifecycle_is_separate_from_fixture_entities(self) -> None: + with tempfile.TemporaryDirectory() as directory: + manifest = Path(directory) / "run-manifest.json" + args = argparse.Namespace( + run_id="SUITE-20260805-abc123", + store="shared", + manifest=str(manifest), + ttl_seconds=60, + ) + original_client = seed_fixtures.WooClient + seed_fixtures.WooClient = LockWooClient + try: + with contextlib.redirect_stdout(io.StringIO()): + seed_fixtures.lock(args) + finally: + seed_fixtures.WooClient = original_client + + saved = json.loads(manifest.read_text(encoding="utf-8")) + + self.assertEqual(202, saved["lock"]["id"]) + self.assertEqual([], saved["entities"]) + + def test_cleanup_journals_each_successful_deletion_before_continuing(self) -> None: + with tempfile.TemporaryDirectory() as directory: + manifest = Path(directory) / "run-manifest.json" + manifest.write_text( + json.dumps( + { + "run_id": "SUITE-20260805-abc123", + "store": "lab", + "entities": [ + {"type": "product", "id": 301, "label": "first"}, + {"type": "product", "id": 302, "label": "second"}, + ], + } + ), + encoding="utf-8", + ) + args = argparse.Namespace(manifest=str(manifest), store=None) + original_client = seed_fixtures.WooClient + seed_fixtures.WooClient = PartiallyFailingCleanupClient + try: + with ( + contextlib.redirect_stderr(io.StringIO()), + self.assertRaisesRegex(seed_fixtures.SmokeSetupError, "1 deletion error"), + ): + seed_fixtures.cleanup(args) + finally: + seed_fixtures.WooClient = original_client + + saved = json.loads(manifest.read_text(encoding="utf-8")) + + self.assertEqual( + [{"type": "product", "id": 301, "label": "first"}], + saved["entities"], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/.maestro/scripts/tests/test_smoke_cli.py b/.maestro/scripts/tests/test_smoke_cli.py new file mode 100644 index 000000000000..1b68fbf9dd74 --- /dev/null +++ b/.maestro/scripts/tests/test_smoke_cli.py @@ -0,0 +1,498 @@ +#!/usr/bin/env python3 +"""Public CLI contract tests for the Android Maestro smoke runner.""" + +from __future__ import annotations + +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + + +SCRIPT_DIR = Path(__file__).resolve().parent +REPO_ROOT = SCRIPT_DIR.parent.parent.parent +RUNNER = REPO_ROOT / ".maestro" / "scripts" / "run-smoke-tests.sh" +DOCTOR = REPO_ROOT / ".maestro" / "scripts" / "doctor.py" +GOLDEN_DIR = SCRIPT_DIR / "golden" + + +class SmokeCliContractTest(unittest.TestCase): + def test_cleanup_finishes_before_reports_and_participates_in_exit_status(self) -> None: + source = RUNNER.read_text(encoding="utf-8") + + self.assertLess( + source.index('echo "--- Cleaning run-owned fixtures"'), + source.index('echo "--- Generating reports"'), + ) + self.assertIn('|| "$CLEANUP_FAILED" -gt 0', source) + self.assertNotIn('cleanup --manifest "$MANIFEST_FILE" --store "$STORE" || true', source) + + def test_destructive_flows_are_not_blindly_retried(self) -> None: + source = RUNNER.read_text(encoding="utf-8") + + self.assertIn('if flow_has_any_tag "$flow" destructive; then', source) + self.assertIn('echo " destructive flow failed; automatic retry is disabled"', source) + + def run_runner(self, *args: str) -> tuple[subprocess.CompletedProcess[str], Path]: + temporary_directory = tempfile.TemporaryDirectory() + self.addCleanup(temporary_directory.cleanup) + output_root = Path(temporary_directory.name) / "output" + env = { + **os.environ, + "PATH": "/usr/bin:/bin", + "WOO_MAESTRO_OUTPUT_DIR": str(output_root), + } + result = subprocess.run( + [str(RUNNER), *args], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + check=False, + ) + return result, output_root + + def run_with_fake_device_tools( + self, + *args: str, + env_overrides: dict[str, str] | None = None, + maestro_version: str = "2.8.0", + ) -> tuple[subprocess.CompletedProcess[str], Path]: + temporary_directory = tempfile.TemporaryDirectory() + self.addCleanup(temporary_directory.cleanup) + temporary_path = Path(temporary_directory.name) + fake_bin = temporary_path / "bin" + fake_bin.mkdir() + adb_marker = temporary_path / "adb-invoked" + + maestro = fake_bin / "maestro" + maestro.write_text( + "#!/bin/sh\n" + "if [ \"${1:-}\" = --version ]; then\n" + f" printf '%s\\n' '{maestro_version}'\n" + "fi\n" + ) + maestro.chmod(0o755) + java = fake_bin / "java" + java.write_text("#!/bin/sh\nprintf '%s\\n' 'openjdk version \"21.0.8\"' >&2\n") + java.chmod(0o755) + adb = fake_bin / "adb" + adb.write_text( + "#!/bin/sh\n" + f": > '{adb_marker}'\n" + "printf 'List of devices attached\\n'\n" + ) + adb.chmod(0o755) + + env = {key: value for key, value in os.environ.items() if not key.startswith("MAESTRO_WOO_")} + env.update( + { + "CI": "1", + "HOME": str(temporary_path), + "PATH": f"{fake_bin}:/usr/bin:/bin", + "WOO_MAESTRO_OUTPUT_DIR": str(temporary_path / "output"), + } + ) + env.update(env_overrides or {}) + result = subprocess.run( + [str(RUNNER), *args], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + check=False, + ) + return result, adb_marker + + def run_with_order_recording_tools( + self, + *args: str, + env_overrides: dict[str, str], + ) -> tuple[subprocess.CompletedProcess[str], list[str]]: + temporary_directory = tempfile.TemporaryDirectory() + self.addCleanup(temporary_directory.cleanup) + temporary_path = Path(temporary_directory.name) + fake_bin = temporary_path / "bin" + fake_bin.mkdir() + events = temporary_path / "events" + + maestro = fake_bin / "maestro" + maestro.write_text( + "#!/bin/sh\n" + "if [ \"${1:-}\" = --version ]; then printf '%s\\n' '2.8.0'; fi\n" + ) + maestro.chmod(0o755) + java = fake_bin / "java" + java.write_text("#!/bin/sh\nprintf '%s\\n' 'openjdk version \"21.0.8\"' >&2\n") + java.chmod(0o755) + adb = fake_bin / "adb" + adb.write_text( + "#!/bin/sh\n" + f"printf 'adb\\n' >> '{events}'\n" + "printf 'List of devices attached\\n'\n" + ) + adb.chmod(0o755) + seed_script = temporary_path / "seed-fixtures" + seed_script.write_text( + "#!/bin/sh\n" + "command=$1\n" + "shift\n" + f"printf '%s\\n' \"$command\" >> '{events}'\n" + "manifest=\n" + "while [ $# -gt 0 ]; do\n" + " if [ \"$1\" = --manifest ]; then manifest=$2; shift 2; else shift; fi\n" + "done\n" + "if [ \"$command\" = lock ] && [ -n \"$manifest\" ]; then\n" + " printf '{\"lock\": {\"id\": 1}, \"entities\": []}\\n' > \"$manifest\"\n" + "fi\n" + ) + seed_script.chmod(0o755) + + env = {key: value for key, value in os.environ.items() if not key.startswith("MAESTRO_WOO_")} + env.update( + { + "CI": "1", + "HOME": str(temporary_path), + "PATH": f"{fake_bin}:/usr/bin:/bin", + "WOO_MAESTRO_OUTPUT_DIR": str(temporary_path / "output"), + "WOO_MAESTRO_SEED_SCRIPT": str(seed_script), + **env_overrides, + } + ) + result = subprocess.run( + [str(RUNNER), *args], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + check=False, + ) + recorded_events = events.read_text().splitlines() if events.exists() else [] + return result, recorded_events + + def run_core_with_recorded_maestro_args(self) -> tuple[subprocess.CompletedProcess[str], str]: + temporary_directory = tempfile.TemporaryDirectory() + self.addCleanup(temporary_directory.cleanup) + temporary_path = Path(temporary_directory.name) + fake_bin = temporary_path / "bin" + fake_bin.mkdir() + maestro_args = temporary_path / "maestro-args" + + maestro = fake_bin / "maestro" + maestro.write_text( + "#!/bin/sh\n" + "if [ \"${1:-}\" = --version ]; then printf '%s\\n' '2.8.0'; exit 0; fi\n" + f"printf 'ARGS:%s\\n' \"$*\" >> '{maestro_args}'\n" + f"env | grep -E '^(MAESTRO_)?WOO_' | sort >> '{maestro_args}'\n" + ) + maestro.chmod(0o755) + java = fake_bin / "java" + java.write_text("#!/bin/sh\nprintf '%s\\n' 'openjdk version \"21.0.8\"' >&2\n") + java.chmod(0o755) + adb = fake_bin / "adb" + adb.write_text( + "#!/bin/sh\n" + "if [ \"${1:-}\" = devices ]; then\n" + " printf 'List of devices attached\\nemulator-5554\\tdevice\\n'\n" + "elif printf '%s\\n' \"$*\" | grep -q 'settings get global'; then\n" + " printf '1\\n'\n" + "fi\n" + ) + adb.chmod(0o755) + + env = {key: value for key, value in os.environ.items() if not key.startswith("MAESTRO_WOO_")} + env.update( + { + "CI": "1", + "HOME": str(temporary_path), + "PATH": f"{fake_bin}:/usr/bin:/bin", + "WOO_MAESTRO_OUTPUT_DIR": str(temporary_path / "output"), + "MAESTRO_WOO_LAB_JETPACK_STORE_URL": "https://lab.example.com/", + "MAESTRO_WOO_LAB_WPCOM_EMAIL": "lab@example.com", + "MAESTRO_WOO_LAB_WPCOM_PASSWORD": "selected-password", + "MAESTRO_WOO_LAB_CONSUMER_SECRET": "selected-rest-secret", + "MAESTRO_WOO_SHARED_WPCOM_PASSWORD": "other-store-secret", + } + ) + result = subprocess.run( + [str(RUNNER), "--profile", "core", "--device", "emulator-5554"], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + check=False, + ) + return result, maestro_args.read_text(encoding="utf-8") if maestro_args.exists() else "" + + def assert_golden(self, result: subprocess.CompletedProcess[str], name: str) -> None: + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stderr, "") + self.assertEqual(result.stdout, (GOLDEN_DIR / name).read_text()) + + def test_core_plan_is_side_effect_free(self) -> None: + result, output_root = self.run_runner("--plan", "--profile", "core") + + self.assert_golden(result, "core-plan.txt") + self.assertFalse(output_root.exists()) + + def test_phone_full_plan_includes_quarantined_phone_flows(self) -> None: + result, _ = self.run_runner("--plan", "--profile", "phone-full") + + self.assert_golden(result, "phone-full-plan.txt") + + def test_release_plan_excludes_quarantine(self) -> None: + result, _ = self.run_runner("--plan", "--profile", "release") + + self.assert_golden(result, "release-plan.txt") + + def test_burst_plan_repeats_release_selection(self) -> None: + result, _ = self.run_runner("--plan", "--profile", "burst") + + self.assert_golden(result, "burst-plan.txt") + + def test_extended_plan_requires_explicit_quarantine_opt_in(self) -> None: + result, _ = self.run_runner( + "--plan", + "--include-tags", + "smoke_extended", + "--include-quarantine", + "--store", + "lab", + ) + + self.assert_golden(result, "smoke-extended-plan.txt") + + def test_doctor_treats_zero_selected_flows_as_fatal(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + temporary_path = Path(temporary_directory) + fake_bin = temporary_path / "bin" + fake_bin.mkdir() + for name, body in { + "maestro": "#!/bin/sh\nprintf '%s\\n' '2.8.0'\n", + "java": "#!/bin/sh\nprintf '%s\\n' 'openjdk version \"21.0.8\"' >&2\n", + "adb": "#!/bin/sh\nprintf 'List of devices attached\\n'\n", + }.items(): + executable = fake_bin / name + executable.write_text(body) + executable.chmod(0o755) + + env = { + **os.environ, + "PATH": f"{fake_bin}:/usr/bin:/bin", + } + result = subprocess.run( + [ + str(DOCTOR), + "--profile", + "core", + "--include-tags", + "does_not_exist", + "--env-file", + str(temporary_path / "missing.env"), + ], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("[FAIL] 0 flow(s) selected for profile core", result.stdout) + + def test_doctor_reports_a_toolchain_mismatch(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + temporary_path = Path(temporary_directory) + fake_bin = temporary_path / "bin" + fake_bin.mkdir() + for name, body in { + "maestro": "#!/bin/sh\nprintf '%s\\n' '2.7.0'\n", + "java": "#!/bin/sh\nprintf '%s\\n' 'openjdk version \"21.0.8\"' >&2\n", + "adb": "#!/bin/sh\nprintf 'List of devices attached\\n'\n", + }.items(): + executable = fake_bin / name + executable.write_text(body) + executable.chmod(0o755) + + env = {**os.environ, "PATH": f"{fake_bin}:/usr/bin:/bin"} + result = subprocess.run( + [ + str(DOCTOR), + "--profile", + "core", + "--env-file", + str(temporary_path / "missing.env"), + ], + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("[FAIL] Maestro version mismatch: expected 2.8.0, actual 2.7.0", result.stdout) + + def test_plan_treats_zero_selected_flows_as_fatal(self) -> None: + result, output_root = self.run_runner( + "--plan", + "--include-tags", + "smoke_extended", + "--store", + "lab", + ) + + self.assertEqual(result.returncode, 1) + self.assertEqual(result.stdout, "") + self.assertEqual(result.stderr, "No flows matched the current filters.\n") + self.assertFalse(output_root.exists()) + + def test_shared_destructive_run_requires_seed_before_adb(self) -> None: + result, adb_marker = self.run_with_fake_device_tools( + "--store", + "shared", + ".maestro/flows/orders_create.yaml", + env_overrides={ + "MAESTRO_WOO_SHARED_JETPACK_STORE_URL": "https://inpersonpayments.wpcomstaging.com/", + "MAESTRO_WOO_SHARED_WPCOM_EMAIL": "shared@example.com", + "MAESTRO_WOO_SHARED_WPCOM_PASSWORD": "shared-password", + "MAESTRO_WOO_SHARED_CONSUMER_KEY": "ck_shared", + "MAESTRO_WOO_SHARED_CONSUMER_SECRET": "cs_shared", + }, + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("Shared destructive runs require --seed", result.stderr) + self.assertFalse(adb_marker.exists()) + + def test_runtime_rejects_a_mismatched_maestro_before_adb(self) -> None: + result, adb_marker = self.run_with_fake_device_tools( + "--store", + "lab", + ".maestro/flows/login_successful.yaml", + maestro_version="2.7.0", + env_overrides={ + "MAESTRO_WOO_LAB_JETPACK_STORE_URL": "https://lab.example.com/", + "MAESTRO_WOO_LAB_WPCOM_EMAIL": "lab@example.com", + "MAESTRO_WOO_LAB_WPCOM_PASSWORD": "lab-password", + }, + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("Maestro version mismatch: expected 2.8.0, actual 2.7.0", result.stderr) + self.assertFalse(adb_marker.exists()) + + def test_lab_and_generic_credentials_cannot_satisfy_shared_destructive_preflight(self) -> None: + result, adb_marker = self.run_with_fake_device_tools( + "--store", + "shared", + "--seed", + ".maestro/flows/orders_create.yaml", + env_overrides={ + "MAESTRO_WOO_LAB_JETPACK_STORE_URL": "https://lab.example.com/", + "MAESTRO_WOO_LAB_WPCOM_EMAIL": "lab@example.com", + "MAESTRO_WOO_LAB_WPCOM_PASSWORD": "lab-password", + "MAESTRO_WOO_LAB_CONSUMER_KEY": "ck_lab", + "MAESTRO_WOO_LAB_CONSUMER_SECRET": "cs_lab", + "MAESTRO_WOO_JETPACK_STORE_URL": "https://lab.example.com/", + "MAESTRO_WOO_WPCOM_EMAIL": "lab@example.com", + "MAESTRO_WOO_WPCOM_PASSWORD": "lab-password", + "MAESTRO_WOO_CONSUMER_KEY": "ck_lab", + "MAESTRO_WOO_CONSUMER_SECRET": "cs_lab", + }, + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("Missing scoped shared store configuration", result.stderr) + self.assertFalse(adb_marker.exists()) + + def test_shared_destructive_preflight_requires_the_exact_shared_host(self) -> None: + result, adb_marker = self.run_with_fake_device_tools( + "--store", + "shared", + "--seed", + ".maestro/flows/orders_create.yaml", + env_overrides={ + "MAESTRO_WOO_SHARED_JETPACK_STORE_URL": "https://lookalike.example.com/", + "MAESTRO_WOO_SHARED_WPCOM_EMAIL": "shared@example.com", + "MAESTRO_WOO_SHARED_WPCOM_PASSWORD": "shared-password", + "MAESTRO_WOO_SHARED_CONSUMER_KEY": "ck_shared", + "MAESTRO_WOO_SHARED_CONSUMER_SECRET": "cs_shared", + }, + ) + + self.assertEqual(result.returncode, 1) + self.assertIn( + "Shared destructive runs require host inpersonpayments.wpcomstaging.com", + result.stderr, + ) + self.assertFalse(adb_marker.exists()) + + def test_shared_destructive_lock_is_acquired_before_adb(self) -> None: + result, events = self.run_with_order_recording_tools( + "--store", + "shared", + "--seed", + ".maestro/flows/orders_create.yaml", + env_overrides={ + "MAESTRO_WOO_SHARED_JETPACK_STORE_URL": "https://inpersonpayments.wpcomstaging.com/", + "MAESTRO_WOO_SHARED_WPCOM_EMAIL": "shared@example.com", + "MAESTRO_WOO_SHARED_WPCOM_PASSWORD": "shared-password", + "MAESTRO_WOO_SHARED_CONSUMER_KEY": "ck_shared", + "MAESTRO_WOO_SHARED_CONSUMER_SECRET": "cs_shared", + }, + ) + + self.assertEqual(result.returncode, 1) + self.assertEqual(events, ["lock", "adb", "unlock"]) + + def test_seed_request_does_not_create_unused_fixtures_without_destructive_flows(self) -> None: + result, events = self.run_with_order_recording_tools( + "--profile", + "release", + "--seed", + env_overrides={ + "MAESTRO_WOO_SHARED_JETPACK_STORE_URL": "https://inpersonpayments.wpcomstaging.com/", + "MAESTRO_WOO_SHARED_WPCOM_EMAIL": "shared@example.com", + "MAESTRO_WOO_SHARED_WPCOM_PASSWORD": "shared-password", + "MAESTRO_WOO_SHARED_CONSUMER_KEY": "ck_shared", + "MAESTRO_WOO_SHARED_CONSUMER_SECRET": "cs_shared", + }, + ) + + self.assertEqual(result.returncode, 1) + self.assertEqual(events, ["adb"]) + self.assertIn("No destructive flows selected; skipping fixture seeding", result.stdout) + + def test_generic_credentials_cannot_satisfy_a_scoped_lab_selection(self) -> None: + result, adb_marker = self.run_with_fake_device_tools( + "--store", + "lab", + ".maestro/flows/login_successful.yaml", + env_overrides={ + "MAESTRO_WOO_JETPACK_STORE_URL": "https://lab.example.com/", + "MAESTRO_WOO_WPCOM_EMAIL": "lab@example.com", + "MAESTRO_WOO_WPCOM_PASSWORD": "lab-password", + }, + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("Missing required env var: MAESTRO_WOO_JETPACK_STORE_URL", result.stderr) + self.assertFalse(adb_marker.exists()) + + def test_maestro_cli_receives_only_selected_flow_values_and_no_rest_or_other_store_secrets(self) -> None: + result, args = self.run_core_with_recorded_maestro_args() + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("WOO_WPCOM_PASSWORD=selected-password", args) + self.assertNotIn("MAESTRO_WOO_WPCOM_PASSWORD", args) + for line in args.splitlines(): + if line.startswith("ARGS:"): + self.assertNotIn("selected-password", line) + self.assertNotIn("selected-rest-secret", args) + self.assertNotIn("other-store-secret", args) + + +if __name__ == "__main__": + unittest.main() diff --git a/.maestro/smoke-coverage.yaml b/.maestro/smoke-coverage.yaml new file mode 100644 index 000000000000..487f37abe4a2 --- /dev/null +++ b/.maestro/smoke-coverage.yaml @@ -0,0 +1,314 @@ +# Maestro smoke coverage snapshot. +# +# Source P2: https://woomobilep2.wordpress.com/flows-for-app-features-smoke-testing/ +# Fetched through context-a8c wpcom/posts-text on 2026-08-05. +# P2 modified timestamp observed: 2026-07-28T06:29:33+00:00. +# +# Every item needs either `flow:` or `manual:`. Flow YAMLs declare covered +# items with `# p2:` headers, and .maestro/scripts/check-smoke-coverage.py +# validates this file offline. + +items: + - id: install.upgrade + title: Upgrade from existing store version + manual: "Requires APK version swap and logged-in state migration outside Maestro." + - id: install.fresh + title: Fresh install smoke + manual: "Requires uninstall/data reset plus first-launch assertions; the runner currently installs with replacement semantics." + + - id: login.help + title: Login help section + flow: .maestro/flows/login_help.yaml + - id: login.not-wp-site + title: Login with a non-WordPress site + flow: .maestro/flows/login_not_wp_site.yaml + - id: login.not-woo-store + title: Login to a WordPress site without WooCommerce + flow: .maestro/flows/login_not_woo_store.yaml + - id: login.no-jetpack + title: Login to WooCommerce site without Jetpack + flow: .maestro/flows/login_no_jetpack.yaml + - id: login.jetpack-not-connected + title: Login with Jetpack installed but not connected + manual: "Requires manually disconnecting Jetpack on a disposable Jurassic Ninja site." + - id: login.jetpack + title: Login to Jetpack-connected WooCommerce store + flow: .maestro/flows/login_successful.yaml + - id: login.wrong-account + title: Wrong account for the store + flow: .maestro/flows/login_wrong_account.yaml + - id: login.wrong-credentials + title: Wrong credentials + flow: .maestro/flows/login_wrong_credentials.yaml + - id: login.passwordless + title: Passwordless login + manual: "Later automation needs Mailosaur API and secret management." + - id: login.social-google + title: Social login with Google + flow: .maestro/flows/login_google.yaml + - id: login.social-apple + title: Social login with Apple + manual: "iOS-only per P2." + - id: login.2fa + title: Login with 2FA + manual: "Needs dedicated test account plus TOTP secret handling." + - id: login.qr.wpcom-session + title: QR login from an active WordPress.com browser session + manual: "Guided only until the browser session and QR handoff are deterministic test fixtures." + - id: login.qr.wp-admin-session + title: QR login from an active wp-admin browser session + manual: "Guided only until the browser session and QR handoff are deterministic test fixtures." + - id: login.qr.mobile-app-modal + title: QR login from the wp-admin install mobile app modal + manual: "Guided only until the wp-admin modal and QR handoff are deterministic test fixtures." + + - id: push-no-jetpack.login + title: Login to a no-Jetpack store for push setup + manual: "Requires a disposable no-Jetpack store and isolated notification account state." + - id: push-no-jetpack.card-visible + title: Never miss a new order card is visible + manual: "Part of the guided no-Jetpack push setup until the store fixture is deterministic." + - id: push-no-jetpack.unlock + title: Open and continue through Unlock push notifications + manual: "Requires deterministic notification permissions and backend setup state." + - id: push-no-jetpack.setup-completes + title: No-Jetpack push setup completes + manual: "Requires deterministic notification permissions and backend setup state." + - id: push-no-jetpack.card-hidden + title: Never miss a new order card is hidden after setup + manual: "Blocked with deterministic no-Jetpack push setup." + - id: push-no-jetpack.notification-arrives + title: Web order triggers a push without Jetpack + manual: "Needs an API-created order and deterministic push-delivery fixture." + - id: push-no-jetpack.notification-deep-link + title: No-Jetpack push opens the correct order + manual: "Blocked with deterministic no-Jetpack push delivery." + + - id: dashboard.stats + title: Dashboard charts and stats respond + flow: .maestro/flows/dashboard_stats.yaml + - id: dashboard.analytics + title: View all store analytics + flow: .maestro/flows/dashboard_view_all_analytics.yaml + - id: dashboard.customization + title: Dashboard card customization + flow: .maestro/flows/dashboard_customize.yaml + + - id: orders.push.arrives + title: New order push notification arrives + manual: "Later automation should trigger an order via API and assert push delivery." + - id: orders.push.deep-link + title: Push opens the correct order + manual: "Blocked with push delivery automation." + - id: orders.list + title: Orders list loads + flow: .maestro/flows/orders_list_and_search.yaml + - id: orders.pagination + title: Orders pagination + manual: "Not automated." + - id: orders.search + title: Search for an order + flow: .maestro/flows/orders_list_and_search.yaml + - id: orders.create.products + title: Create order with products + flow: .maestro/flows/orders_create.yaml + - id: orders.create.variable-products + title: Create order with variable products + flow: .maestro/flows/orders_create.yaml + - id: orders.create.quantity + title: Increase and decrease quantity + flow: .maestro/flows/orders_create.yaml + - id: orders.create.product-discount + title: Apply product discount + flow: .maestro/flows/orders_create.yaml + - id: orders.create.custom-amount + title: Add custom amount + flow: .maestro/flows/orders_create.yaml + - id: orders.create.custom-amount-note + title: Add custom amount with note + flow: .maestro/flows/orders_create.yaml + - id: orders.create.shipping + title: Add shipping + flow: .maestro/flows/orders_create.yaml + - id: orders.create.customer-add + title: Add existing customer + flow: .maestro/flows/orders_create.yaml + - id: orders.create.customer-edit + title: Edit existing customer + flow: .maestro/flows/orders_create.yaml + - id: orders.create.note + title: Add customer note during order creation + flow: .maestro/flows/orders_create.yaml + - id: orders.barcode.open-scanner + title: Barcode button opens scanner + flow: .maestro/flows/orders_barcode_scanner_opens.yaml + - id: orders.barcode.add-product + title: Add product using barcode scanner + manual: "Requires camera or guided manual mode." + - id: orders.collect-payment.qr + title: Scan to Pay displays QR code + flow: .maestro/flows/orders_payment_qr_and_share.yaml + - id: orders.collect-payment.share-link + title: Share payment link + flow: .maestro/flows/orders_payment_qr_and_share.yaml + - id: orders.collect-payment.cash + title: Cash payment method + flow: .maestro/flows/orders_cash_payment.yaml + - id: orders.receipt + title: See receipt from order detail + flow: .maestro/flows/orders_details_and_actions.yaml + - id: orders.refund + title: Refund an order + flow: .maestro/flows/orders_refund.yaml + - id: orders.note + title: Add order note + flow: .maestro/flows/orders_details_and_actions.yaml + - id: orders.shipping-label + title: Create a shipping label + manual: "External shipping-label flow is outside current smoke scope." + - id: orders.mark-complete + title: Mark order complete + flow: .maestro/flows/orders_mark_complete.yaml + - id: orders.barcode.start-order + title: Start order creation by scanning barcode + manual: "Requires camera or guided manual mode." + + - id: products.list + title: Product list loads + flow: .maestro/flows/products_list_and_sort.yaml + - id: products.pagination + title: Product pagination + manual: "Not automated." + - id: products.sort + title: Sort product list + flow: .maestro/flows/products_list_and_sort.yaml + - id: products.search + title: Search product list + flow: .maestro/flows/products_list_and_sort.yaml + - id: products.detail.description + title: Product description details + flow: .maestro/flows/products_create.yaml + - id: products.detail.media + title: Product media upload entry point + flow: .maestro/flows/products_media_upload.yaml + - id: products.detail.price + title: Product price settings + flow: .maestro/flows/products_create.yaml + - id: products.detail.inventory + title: Product inventory settings + flow: .maestro/flows/products_create.yaml + - id: products.detail.categories + title: Product categories settings + flow: .maestro/flows/products_detail.yaml + - id: products.detail.type + title: Product type settings + flow: .maestro/flows/products_create.yaml + - id: products.detail.tags + title: Product tags settings + flow: .maestro/flows/products_create.yaml + - id: products.detail.shipping + title: Product shipping settings + flow: .maestro/flows/products_create.yaml + - id: products.detail.short-description + title: Product short description settings + flow: .maestro/flows/products_create.yaml + - id: products.detail.linked + title: Linked products + flow: .maestro/flows/products_create.yaml + - id: products.detail.downloads + title: Downloadable files + flow: .maestro/flows/products_create.yaml + - id: products.detail.variations + title: Variations and variation detail + flow: .maestro/flows/products_variations_and_tags.yaml + - id: products.detail.variation-attributes + title: Variation attributes + flow: .maestro/flows/products_variations_and_tags.yaml + - id: products.create + title: Create product + flow: .maestro/flows/products_create.yaml + + - id: hub.change-store + title: Change store + flow: .maestro/flows/hub_menu_admin_and_store.yaml + - id: hub.settings + title: Hub settings + flow: .maestro/flows/hub_menu_settings.yaml + - id: hub.payments + title: Payments hub + flow: .maestro/flows/hub_menu_payments.yaml + - id: hub.coupons.create + title: Create a coupon + flow: .maestro/flows/hub_menu_coupons.yaml + - id: hub.customers + title: Customers + flow: .maestro/flows/hub_menu_customers_inbox.yaml + - id: hub.inbox + title: Inbox + flow: .maestro/flows/hub_menu_customers_inbox.yaml + - id: hub.wc-admin + title: WC Admin + flow: .maestro/flows/hub_menu_admin_and_store.yaml + - id: hub.view-store + title: View Store + flow: .maestro/flows/hub_menu_admin_and_store.yaml + - id: hub.blaze.create + title: Blaze campaign creation entry point + manual: "Feature-gated probe only; an ineligible store must not count as coverage." + - id: hub.google-for-woo + title: Google for Woo campaign webview + manual: "Feature-gated probe only; an ineligible store must not count as coverage." + + - id: payments.card-reader + title: Collect payment using card reader + manual: "Hardware-dependent; later guided-manual mode." + - id: payments.ttp + title: Collect payment using Tap to Pay + manual: "Hardware/account-dependent; later guided-manual mode." + - id: payments.ipp-refund + title: Refund an IPP order + manual: "Hardware/payment-account dependent." + + - id: pos.search-products + title: POS search products + flow: .maestro/flows/pos_search_and_coupons.yaml + - id: pos.add-products + title: POS add products to cart + flow: .maestro/flows/pos_cash_payment.yaml + - id: pos.coupons + title: POS use coupons + flow: .maestro/flows/pos_search_and_coupons.yaml + - id: pos.pay-card + title: POS pay with card + manual: "Card reader hardware-dependent; later guided-manual mode." + - id: pos.pay-cash + title: POS pay with cash + flow: .maestro/flows/pos_cash_payment.yaml + - id: pos.email-receipts + title: POS send and check email receipts + manual: "Requires sending and checking a real receipt email." + + - id: other.localization + title: Switch device to non-English language + manual: "Requires locale matrix run; selectors are prepared to avoid text-driven navigation." + - id: other.home-widget + title: Home screen widgets + manual: "Outside Maestro app surface." + - id: other.quick-actions + title: Quick Actions + flow: .maestro/flows/android_quick_actions.yaml + - id: other.ios-push-long-press + title: Long press iPhone push notification + manual: "iOS-only per P2." + - id: other.watch.my-store + title: Watch app My Store + manual: "Watch app stays manual per plan." + - id: other.watch.orders-list + title: Watch app order list + manual: "Watch app stays manual per plan." + - id: other.watch.order-detail + title: Watch app order detail + manual: "Watch app stays manual per plan." + - id: other.watch.push + title: Watch push notification opens order details + manual: "Watch app stays manual per plan." diff --git a/.maestro/strings.env b/.maestro/strings.env new file mode 100644 index 000000000000..07dfc6653f29 --- /dev/null +++ b/.maestro/strings.env @@ -0,0 +1,4508 @@ +# Generated by .maestro/scripts/generate-strings-env.py +# Source: WooCommerce/src/main/res/values/strings.xml + +STRING_APP_NAME='Woo' +STRING_NOTIFICATION_CHANNEL_GENERAL_TITLE='General' +STRING_NOTIFICATION_CHANNEL_ORDER_TITLE='New order alerts' +STRING_NOTIFICATION_CHANNEL_REVIEW_TITLE='Product review alerts' +STRING_NOTIFICATION_CHANNEL_STOCK_TITLE='Stock alerts' +STRING_ALL='All' +STRING_LOGGING_IN='Logging in' +STRING_LOADING_STORES='Loading stores' +STRING_SIGNOUT='Log out' +STRING_MY_STORE='My store' +STRING_ORDERS='Orders' +STRING_EMAIL_ADDRESS='Email address' +STRING_EMAIL='Email' +STRING_ORDER_TOTAL='Order total' +STRING_SHIPPING='Shipping' +STRING_MULTIPLE_SHIPPING='multiple shipping lines' +STRING_EDIT='Edit' +STRING_PAYMENT='Payment' +STRING_TAXES='Taxes' +STRING_TAX='Tax' +STRING_TOTAL='Total' +STRING_SUBTOTAL='Subtotal' +STRING_PRODUCTS_TOTAL='Products total' +STRING_DISCOUNT='Discount' +STRING_DETAILS='Details' +STRING_SHOW_DETAILS='Show Details' +STRING_HIDE_DETAILS='Hide Details' +STRING_RETRY='Retry' +STRING_UNDO='Undo' +STRING_INSTALL='Install' +STRING_UPDATE_DOWNLOADED='Woo has downloaded an update' +STRING_UPDATE_FAILED='Woo update has failed' +STRING_CONTINUE_BUTTON='Continue' +STRING_REFRESH_BUTTON='Refresh' +STRING_UNTITLED='Untitled' +STRING_DIALOG_OK='OK' +STRING_CANCEL='Cancel' +STRING_YES='Yes' +STRING_NO='No' +STRING_CANCEL_ANYWAY='Cancel anyway' +STRING_KEEP_EDITING='Keep editing' +STRING_KEEP_CHANGES='Keep changes' +STRING_LEARN_MORE='Learn more' +STRING_SET_UP_NOW='Set up now' +STRING_OFFLINE_MESSAGE='Offline \u2014 using cached data' +STRING_OFFLINE_ERROR='Your network is unavailable. Check your data or wifi connection.' +STRING_WOO_POS_PTR_OFFLINE_ERROR='No internet connection. Please check your network and try again.' +STRING_PRODUCT='Product' +STRING_TODAY='Today' +STRING_THIS_WEEK='This Week' +STRING_THIS_MONTH='This Month' +STRING_THIS_YEAR='This Year' +STRING_DISCARD='Discard' +STRING_PRODUCTS='Products' +STRING_POINT_OF_SALE='Point of Sale' +STRING_CUSTOM_AMOUNTS='Custom Amounts' +STRING_REFUNDS='Refunds' +STRING_SOMETHING_WENT_WRONG_TRY_AGAIN='Something went wrong, Please try again later.' +STRING_LOADING='Loading…' +STRING_SHIPPING_LABELS='Shipping Labels' +STRING_PRODUCT_VARIATIONS='Variations' +STRING_PRODUCT_VARIATION_OPTIONS_RE='.*\ \(.*\ options\)' +STRING_PRODUCT_VARIATION_MULTIPLE_COUNT_RE='.*\ variations' +STRING_PRODUCT_VARIATION_SINGLE_COUNT='1 variation' +STRING_PRODUCT_VARIATION_ATTRIBUTES='Attributes' +STRING_PRODUCT_ADD_ATTRIBUTE='Add attribute' +STRING_PRODUCT_NEW_ATTRIBUTE_NAME='New attribute name' +STRING_PRODUCT_RENAME_ATTRIBUTE='Rename attribute' +STRING_PRODUCT_RENAME_ATTRIBUTE_HELPER='Type of variation, eg. size or color' +STRING_PRODUCT_CREATE_ATTRIBUTE_HELPER='To create a variation, you'"'"'ll need to set its attributes (ie "Color", "Size") first' +STRING_PRODUCT_SELECT_ATTRIBUTE='Or tap to select an existing attribute' +STRING_PRODUCT_NEW_ATTRIBUTE_TERM_NAME='Option name' +STRING_PRODUCT_SELECT_ATTRIBUTE_TERM='Or tap to select an existing option' +STRING_PRODUCT_ENTER_ATTRIBUTE_TERM='Add each option name and press enter' +STRING_PRODUCT_ATTRIBUTE_NAME_ALREADY_EXISTS='An attribute with this name already exists' +STRING_PRODUCT_ANY_ATTRIBUTE_HINT='Any' +STRING_PRODUCT_TERM_NAME_ALREADY_EXISTS='An option with this name already exists' +STRING_PRODUCT_ATTRIBUTE_ERROR_RENAMING='Error while renaming your attribute' +STRING_PRODUCT_ATTRIBUTES_ERROR_SAVING='Error while saving your attributes' +STRING_PRODUCT_ATTRIBUTE_REMOVE='Remove this attribute?' +STRING_PRODUCT_BULK_UPDATE_REGULAR_PRICE='Update regular price' +STRING_PRODUCT_BULK_UPDATE_PRICE_UPDATED='Price updated!' +STRING_PRODUCT_BULK_UPDATE_STATUS='Update status' +STRING_PRODUCT_BULK_UPDATE_STATUS_UPDATED='Status updated!' +STRING_REVIEW_NOTIFICATIONS='Reviews' +STRING_VERSION_WITH_NAME_PARAM_RE='Version\ .*' +STRING_SHARE_STORE_BUTTON='Share your store' +STRING_SHARE_STORE_DIALOG_TITLE='Share your store'"'"'s URL' +STRING_SHARE='Share' +STRING_SEARCH='Search' +STRING_SCAN_BARCODE='Scan Barcode' +STRING_CLEAR='Clear' +STRING_DONE='Done' +STRING_BACK='Back' +STRING_CLOSE='Close' +STRING_BUTTON_UPDATE_INSTRUCTIONS='Update instructions' +STRING_DISMISS='Dismiss' +STRING_ALLOW='Allow' +STRING_APPLY='Apply' +STRING_COPIED_TO_CLIPBOARD='Copied to clipboard' +STRING_ERROR_COPY_TO_CLIPBOARD='Error copying to clipboard' +STRING_READ_MORE='Read more' +STRING_DATABASE_DOWNGRADED='Database downgraded, recreating tables and loading stores' +STRING_REMOVE='Remove' +STRING_RENAME='Rename' +STRING_TYPE='Type' +STRING_TRY_AGAIN='Try again' +STRING_UPDATE='Update' +STRING_SAVE='Save' +STRING_SKIP='Skip' +STRING_DISCARD_MESSAGE='Do you want to discard your changes?' +STRING_DISCARD_IMAGES_MESSAGE='Product images are still uploading. Do you want to discard your changes?' +STRING_MORE_OPTIONS='More options' +STRING_VALUE_NOT_SET='Not set' +STRING_SELECTION_COUNT_RE='.*\ selected' +STRING_PLEASE_WAIT='Please wait…' +STRING_OTHER='Other' +STRING_NA='N/A' +STRING_FREE='free' +STRING_ERROR_REQUIRED_FIELD='Required field' +STRING_ANALYTICS='Analytics' +STRING_CUSTOMIZE_ANALYTICS='Customize Analytics' +STRING_DRAG_HANDLE='Drag handle' +STRING_CASH='Cash' +STRING_CREATE='Create' +STRING_AMOUNT='Amount' +STRING_COPY='Copy' +STRING_VARIATIONS_BULK_UPDATE_SALE_PRICE='Update Sale Price' +STRING_VARIATIONS_BULK_UPDATE_REGULAR_PRICE='Update Regular Price' +STRING_TIP='Tip' +STRING_HIDE_PASSWORD_CONTENT_DESCRIPTION='Hide password' +STRING_SHOW_PASSWORD_CONTENT_DESCRIPTION='Show password' +STRING_LAST_UPDATE_RE='Last\ update:\ .*' +STRING_LAST_UPDATE_WITH_FREQUENCY_RE='Last\ update\ .*\ \(Updates\ every\ 30\ minutes\)' +STRING_RECEIPT_FETCHING_ERROR='Sorry, we couldn'"'"'t load a receipt for this order' +STRING_STORE_NAME_DEFAULT='Store name' +STRING_SORTED_BY_RE='Sorted\ by\ 1.*' +STRING_ADD_RE='Add\ .*' +STRING_BARCODE_EAN13_CONTENT_DESCRIPTION='Barcode EAN13' +STRING_DATE_TIMEFRAME_FUTURE='Upcoming' +STRING_DATE_TIMEFRAME_CUSTOM='Custom' +STRING_DATE_TIMEFRAME_TODAY='Today' +STRING_DATE_TIMEFRAME_YESTERDAY='Yesterday' +STRING_DATE_TIMEFRAME_LAST_WEEK='Last Week' +STRING_DATE_TIMEFRAME_LAST_MONTH='Last Month' +STRING_DATE_TIMEFRAME_LAST_QUARTER='Last Quarter' +STRING_DATE_TIMEFRAME_LAST_YEAR='Last Year' +STRING_DATE_TIMEFRAME_WEEK_TO_DATE='Week to Date' +STRING_DATE_TIMEFRAME_MONTH_TO_DATE='Month to Date' +STRING_DATE_TIMEFRAME_QUARTER_TO_DATE='Quarter to Date' +STRING_DATE_TIMEFRAME_YEAR_TO_DATE='Year to Date' +STRING_DATE_TIMEFRAME_OLDER_TWO_DAYS='Older than 2 days' +STRING_DATE_TIMEFRAME_OLDER_WEEK='Older than a week' +STRING_DATE_TIMEFRAME_OLDER_MONTH='Older than a month' +STRING_DATE_COMPARED_TO='Compared to' +STRING_DATE_TIME_CONNECTOR='at' +STRING_IMAGES_UNAVAILABLE_NOTICE='The images are unavailable because your site is marked Private. You can change this by switching to Coming Soon mode.\nTap to learn more.' +STRING_ERROR_CANT_OPEN_URL='Unable to open the link' +STRING_ERROR_PLEASE_CHOOSE_BROWSER='Error opening the default web browser. Please choose another app:' +STRING_ERROR_NO_PHONE_APP='No phone app was found' +STRING_ERROR_NO_GMAPS_APP='Google Maps app was found' +STRING_ERROR_NO_EMAIL_APP='No e-mail app was found' +STRING_ERROR_NO_SMS_APP='No SMS app was found' +STRING_PAYMENT_METHOD_AMERICAN_EXPRESS='American Express' +STRING_PAYMENT_METHOD_DISCOVER='Discover' +STRING_PAYMENT_METHOD_MASTERCARD='MasterCard' +STRING_PAYMENT_METHOD_VISA='VISA' +STRING_PAYMENT_METHOD_PAYPAL='Paypal' +STRING_ENTER_SITE_ADDRESS='Enter the address of the WooCommerce store you'"'"'d like to connect.' +STRING_ALREADY_LOGGED_IN_WPCOM='You'"'"'re already logged in a WordPress.com account, you can'"'"'t add a WordPress.com site bound to another account.' +STRING_LOGIN_NO_JETPACK_USERNAME_RE='Signed\ in\ as\ @.*\\nWrong\ account\?\ .*' +STRING_LOGIN_WPCOM='Continue with WordPress.com' +STRING_LOGIN_STORE_ADDRESS='Log In' +STRING_LOGIN_APPLICATION_PASSWORDS_HELP='What are Application Passwords?' +STRING_LOGIN_PROLOGUE_LABEL_ANALYTICS='Track sales and high performing products' +STRING_LOGIN_PROLOGUE_LABEL_ORDERS='Manage and edit orders on the go' +STRING_LOGIN_PROLOGUE_LABEL_PRODUCTS='Edit and add new products from anywhere' +STRING_LOGIN_PROLOGUE_LABEL_ANALYTICS_SUBTITLE='We know it'"'"'s essential to your business.' +STRING_LOGIN_PROLOGUE_LABEL_ORDERS_SUBTITLE='You can manage them quickly and easily.' +STRING_LOGIN_PROLOGUE_LABEL_PRODUCTS_SUBTITLE='We enable you to process them effortlessly.' +STRING_LOGIN_PROLOGUE_START_NEW_STORE='Starting a new store?' +STRING_LOGIN_QR_PROLOGUE_TITLE='Scan to log in' +STRING_LOGIN_QR_PROLOGUE_SUBTITLE='On your computer, visit:' +STRING_LOGIN_QR_PROLOGUE_URL_CLIPBOARD_LABEL='WooCommerce login URL' +STRING_LOGIN_QR_PROLOGUE_URL_COPIED='Link copied to clipboard' +STRING_LOGIN_QR_PROLOGUE_STEP_HINT='Then scan the QR code that appears.' +STRING_LOGIN_QR_PROLOGUE_SCAN_BUTTON='Scan QR code' +STRING_LOGIN_QR_PROLOGUE_FALLBACK_LINK='No computer? Log in with site address' +STRING_LOGIN_QR_PROLOGUE_CAMERA_DENIED_TITLE='Camera access needed' +STRING_LOGIN_QR_PROLOGUE_CAMERA_DENIED_BODY='Without camera access, you can still log in by entering your store address.' +STRING_LOGIN_QR_PROLOGUE_CAMERA_DENIED_ALLOW_BUTTON='Allow camera access' +STRING_LOGIN_QR_PROLOGUE_CAMERA_BLOCKED_TITLE='Camera access is turned off' +STRING_LOGIN_QR_PROLOGUE_CAMERA_BLOCKED_BODY='Enable camera access in Settings or cancel to sign in with your store address instead.' +STRING_LOGIN_QR_PROLOGUE_CAMERA_BLOCKED_SETTINGS_BUTTON='Open Permissions Settings' +STRING_LOGIN_QR_SCANNER_HINT='Center the QR code in the frame' +STRING_LOGIN_QR_SCANNER_VISIT_URL_RE='Visit\ .*' +STRING_LOGIN_QR_SCANNER_AUTHENTICATING='Signing you in…' +STRING_LOGIN_QR_ENDPOINT_MISSING_TITLE='This store can'"'"'t complete QR login' +STRING_LOGIN_QR_ENDPOINT_MISSING_BODY='Your WooCommerce plugin doesn'"'"'t support QR login. Please update WooCommerce on your store and try again, or sign in by entering your site URL.' +STRING_LOGIN_QR_ENDPOINT_MISSING_ENTER_URL='Enter site URL instead' +STRING_LOGIN_QR_ENDPOINT_MISSING_RETRY='Try scanning again' +STRING_LOGIN_QR_MATCH_TITLE='Confirm sign-in' +STRING_LOGIN_QR_MATCH_HOST_LABEL='You'"'"'re signing in to' +STRING_LOGIN_QR_MATCH_ACCOUNT_LABEL='You'"'"'re signing in as' +STRING_LOGIN_QR_MATCH_SUBTITLE='Tap this number on your computer to finish.' +STRING_LOGIN_QR_MATCH_COUNTDOWN_RE='Expires\ in\ .*s' +STRING_LOGIN_QR_MATCH_CANCEL='Cancel' +STRING_LOGIN_QR_MATCH_SECURITY_NOTE='Both screens must show the same number for sign-in to complete.' +STRING_LOGIN_QR_SCANNER_ERROR_GENERIC_TITLE='Couldn'"'"'t read that code' +STRING_LOGIN_QR_SCANNER_ERROR_GENERIC_BODY='We couldn'"'"'t read that QR code. Please try again.' +STRING_LOGIN_QR_SCANNER_ERROR_PAYLOAD_TITLE='Not a WooCommerce code' +STRING_LOGIN_QR_SCANNER_ERROR_PAYLOAD_BODY='This QR isn'"'"'t a WooCommerce login code. Generate a new one from your store and try again.' +STRING_LOGIN_QR_SCANNER_ERROR_INSTALL_QR_TITLE='That'"'"'s the install QR' +STRING_LOGIN_QR_SCANNER_ERROR_INSTALL_QR_BODY_RE='The\ app'"'"'s\ already\ installed\ —\ this\ QR\ installs\ it\.\ In\ wp\-admin,\ tap\ the\ .*\ button\ to\ reveal\ the\ sign\-in\ QR\.\ Or\ visit\ .*\ on\ your\ computer\.' +STRING_LOGIN_QR_SCANNER_ERROR_INSTALL_QR_BODY_BUTTON='App is installed' +STRING_LOGIN_QR_SCANNER_ERROR_TOKEN_TITLE='Code expired' +STRING_LOGIN_QR_SCANNER_ERROR_TOKEN_BODY='That code expired or has already been used. Generate a new one in your store.' +STRING_LOGIN_QR_SCANNER_ERROR_RATE_LIMITED_TITLE='Too many attempts' +STRING_LOGIN_QR_SCANNER_ERROR_RATE_LIMITED_BODY='Please wait a few minutes and try again.' +STRING_LOGIN_QR_SCANNER_ERROR_NETWORK_TITLE='Couldn'"'"'t reach your store' +STRING_LOGIN_QR_SCANNER_ERROR_NETWORK_BODY='Check your connection and try again.' +STRING_LOGIN_QR_SCANNER_ERROR_SERVER_TITLE='Something went wrong' +STRING_LOGIN_QR_SCANNER_ERROR_SERVER_BODY='Your store returned an unexpected error. Please try again in a moment.' +STRING_LOGIN_QR_SCANNER_ERROR_SITE_AUTH_TITLE='Sign-in failed' +STRING_LOGIN_QR_SCANNER_ERROR_SITE_AUTH_BODY='We couldn'"'"'t authenticate with your store. Please try again.' +STRING_LOGIN_QR_SCANNER_ERROR_NOT_WOO_TITLE='Not a WooCommerce store' +STRING_LOGIN_QR_SCANNER_ERROR_NOT_WOO_BODY='This site doesn'"'"'t have WooCommerce installed.' +STRING_LOGIN_QR_SCANNER_ERROR_USER_ROLE_TITLE='Permission needed' +STRING_LOGIN_QR_SCANNER_ERROR_USER_ROLE_BODY='Your account doesn'"'"'t have permission to use the app for this store.' +STRING_LOGIN_QR_SCANNER_ERROR_MATCH_REJECTED_TITLE='Sign-in denied' +STRING_LOGIN_QR_SCANNER_ERROR_MATCH_REJECTED_BODY='For your security, this sign-in attempt was cancelled. Generate a new code in your store to try again.' +STRING_LOGIN_QR_SCANNER_ERROR_MATCH_TIMED_OUT_TITLE='Sign-in timed out' +STRING_LOGIN_QR_SCANNER_ERROR_MATCH_TIMED_OUT_BODY='You didn'"'"'t confirm in time. Generate a new code in your store and try again.' +STRING_LOGIN_QR_SCANNER_ERROR_MATCH_ALREADY_SCANNED_TITLE='Code already used' +STRING_LOGIN_QR_SCANNER_ERROR_MATCH_ALREADY_SCANNED_BODY='That code has already been scanned. Generate a new one in your store.' +STRING_LOGIN_QR_SCANNER_ERROR_MATCH_INVALID_GRANT_TITLE='Sign-in interrupted' +STRING_LOGIN_QR_SCANNER_ERROR_MATCH_INVALID_GRANT_BODY='Your sign-in was interrupted. Generate a new code in your store and try again.' +STRING_LOGIN_QR_SCANNER_ERROR_MATCH_ALREADY_COMPLETED_TITLE='Already signed in elsewhere' +STRING_LOGIN_QR_SCANNER_ERROR_MATCH_ALREADY_COMPLETED_BODY='This sign-in was already completed on another device. Generate a new code if you need to try again.' +STRING_LOGIN_QR_ERROR_PRIMARY_RETRY='Try again' +STRING_LOGIN_QR_ERROR_PRIMARY_SCAN='Scan a new code' +STRING_LOGIN_QR_SESSION_REPLACE_TITLE='You'"'"'re already signed in' +STRING_LOGIN_QR_SESSION_REPLACE_BODY='Continuing will sign you out of your current session and start a new sign-in.' +STRING_LOGIN_QR_SESSION_REPLACE_CONTINUE='Continue and sign out' +STRING_LOGIN_QR_SESSION_REPLACE_CANCEL='Cancel' +STRING_LOGIN_PICK_STORE='Select store to connect' +STRING_LOGIN_NON_WOO_STORES_LABEL='Other sites' +STRING_LOGIN_VERIFYING_SITE='Verifying site…' +STRING_LOGIN_VERIFYING_SITE_ERROR_RE='Cannot\ connect\ to\ .*' +STRING_LOGIN_VERIFYING_SITE_JETPACK_TIMEOUT_ERROR_TITLE='Connection Error' +STRING_LOGIN_VERIFYING_SITE_JETPACK_TIMEOUT_ERROR_DESCRIPTION='We were unable to connect to your site. Please contact support to troubleshoot the problem.' +STRING_LOGIN_UPDATE_REQUIRED_TITLE='Update Store to WooCommerce 3.5' +STRING_LOGIN_UPDATE_REQUIRED_DESC='This store uses an older version of WooCommerce. Please upgrade your store to WooCommerce 3.5 or greater to use it with this app.' +STRING_LOGIN_AVATAR_CONTENT_DESCRIPTION='Your profile photo' +STRING_LOGIN_NO_STORES_HEADER='Create your first store' +STRING_LOGIN_NO_STORES_SUBTITLE='Quickly get up and selling with a beautiful online store.' +STRING_LOGIN_WPCOM_ACCOUNT_MISMATCH_RE='It\ looks\ like\ .*\ is\ connected\ to\ a\ different\ WordPress\.com\ account\.' +STRING_LOGIN_JETPACK_NOT_CONNECTED_RE='It\ looks\ like\ your\ account\ is\ not\ connected\ to\ .*'"'"'s\ Jetpack' +STRING_LOGIN_TRY_ANOTHER_ACCOUNT='Log in with another account' +STRING_LOGIN_TRY_ANOTHER_STORE='Try another store' +STRING_LOGIN_NOT_WOO_STORE_RE='It\ looks\ like\ .*\ is\ not\ a\ WooCommerce\ site\.' +STRING_LOGIN_INSTALL_WOO='Install WooCommerce' +STRING_LOGIN_OPEN_INSTALLATION_PAGE='Open installation page' +STRING_LOGIN_NO_JETPACK_RE='To\ use\ this\ app\ for\ .*\ you'"'"'ll\ need\ to\ have\ the\ Jetpack\ plugin\ setup\ and\ connected\ to\ a\ WordPress\.com\ account' +STRING_LOGIN_NOT_WORDPRESS_SITE_V2='We were not able to detect a WordPress site at the address you entered. Please make sure WordPress is installed and that you are running the latest available version.' +STRING_LOGIN_SITE_INFO_FALLBACK_DIALOG_TITLE='Connection Issue' +STRING_LOGIN_SITE_INFO_FALLBACK_DIALOG_MESSAGE='We couldn'"'"'t verify your site details. You can try logging in with your site credentials instead.' +STRING_LOGIN_SITE_INFO_FALLBACK_DIALOG_CTA='Log In with Site Credentials' +STRING_LOGIN_VIEW_CONNECTED_STORES='View connected stores' +STRING_LOGIN_JETPACK_WHAT_IS='What is Jetpack?' +STRING_LOGIN_JETPACK_WHAT_IS_DESCRIPTION='Jetpack is a free WordPress plugin that connects your store with the tools needed to give you the best mobile experience, including push notifications and stats' +STRING_LOGIN_JETPACK_NOT_FOUND='Jetpack not found. Please try again' +STRING_LOGIN_JETPACK_INSTALL='Install Jetpack' +STRING_LOGIN_NEED_HELP_FINDING_EMAIL='Need help finding the required email?' +STRING_LOGIN_EMAIL_HELP_TITLE='What email do I use to sign in?' +STRING_LOGIN_EMAIL_HELP_DESC_RE='In\ your\ site\ admin\ you\ can\ find\ the\ email\ you\ used\ to\ connect\ to\ WordPress\.com\ from\ the\ .*Jetpack\ Dashboard.*\ under\ .*Connections\ >\ Account\ connection.*' +STRING_LOGIN_DISCOVERY_ERROR_TITLE='Connection error' +STRING_LOGIN_DISCOVERY_ERROR_OPTIONS='Here are a few other things you can try:' +STRING_LOGIN_WITH_WORDPRESS='Sign in with WordPress.com' +STRING_LOGIN_TROUBLESHOOTING_TIPS='Read our troubleshooting tips' +STRING_LOGIN_NO_WPCOM_ACCOUNT_FOUND_TITLE='Wrong email' +STRING_LOGIN_NO_WPCOM_ACCOUNT_FOUND_MESSAGE='User does not exist. Verify the email address is correct or try with a different account.' +STRING_USER_ROLE_ACCESS_ERROR_USER_ROLES_NULL='This app supports only Administrator and Shop Manager user roles. We were unable to fetch user roles for your account. Please contact support.' +STRING_USER_ROLE_ACCESS_ERROR_FETCH_FAILED='We couldn'"'"'t verify your role on this store. Please try again. If the problem persists, contact support.' +STRING_USER_ROLE_ACCESS_ERROR_MSG='This app supports only Administrator and Shop Manager user roles. Please contact your store owner to upgrade your role.' +STRING_USER_ROLE_ACCESS_ERROR_LINK='Learn more about roles and permissions' +STRING_USER_ROLE_ACCESS_ERROR_RETRY='You don'"'"'t have the correct user role' +STRING_USER_ACCESS_VERIFYING='Verifying role…' +STRING_LOGIN_SIMPLE_WPCOM_SITE_RE='The\ site\ .*\ is\ currently\ on\ a\ WordPress\.com\ plan\ that\ does\ not\ support\ plugin\ installation\.\ Please\ upgrade\ your\ plan\ to\ use\ WooCommerce\.' +STRING_LOGIN_ACCOUNT_MISMATCH_CONNECT_JETPACK='Connect Jetpack to your account' +STRING_LOGIN_ACCOUNT_MISMATCH_CONNECT_WPCOM='Connect to the site' +STRING_LOGIN_ACCOUNT_MISMATCH_CONNECT_WPCOM_DIALOG_TITLE='Connecting to a WordPress.com site' +STRING_LOGIN_ACCOUNT_MISMATCH_CONNECT_WPCOM_DIALOG_MESSAGE='Please contact the site owner for an invitation to the site as a shop manager or administrator to use the app.' +STRING_LOGIN_JETPACK_INSTALLATION_SCREEN_TITLE='Connect store' +STRING_LOGIN_JETPACK_INSTALLATION_EXPLANATION='Please install the free Jetpack plugin to access your store on this app.' +STRING_LOGIN_JETPACK_CONNECTION_EXPLANATION='Please connect your store to Jetpack to access it on this app.' +STRING_LOGIN_JETPACK_INSTALLATION_CREDENTIALS_HINT='Have your store credentials ready.' +STRING_LOGIN_JETPACK_CONNECT='Connect Jetpack' +STRING_LOGIN_JETPACK_INSTALLATION_ENTER_SITE_CREDENTIALS_RE='Log\ in\ to\ .*\ with\ your\ store\ credentials\ to\ install\ Jetpack\.' +STRING_LOGIN_JETPACK_CONNECTION_ENTER_SITE_CREDENTIALS_RE='Log\ in\ to\ .*\ with\ your\ store\ credentials\ to\ connect\ Jetpack\.' +STRING_LOGIN_JETPACK_USER_ALREADY_CONNECTED_DIALOG_TITLE='Account already connected' +STRING_LOGIN_JETPACK_USER_ALREADY_CONNECTED_DIALOG_MESSAGE='This WordPress admin account is already connected to a different WordPress.com account.\nContinuing with that account will log you out of your current account.' +STRING_LOGIN_JETPACK_USER_ALREADY_CONNECTED_DIALOG_PROCEED_BUTTON='Proceed' +STRING_LOGIN_JETPACK_USER_ALREADY_CONNECTED_DIALOG_CANCEL_BUTTON='Cancel' +STRING_LOGIN_JETPACK_STEPS_INSTALLING='Installing Jetpack' +STRING_LOGIN_JETPACK_STEPS_ACTIVATING='Activating' +STRING_LOGIN_JETPACK_STEPS_AUTHORIZING='Connect store to Jetpack' +STRING_LOGIN_JETPACK_STEPS_AUTHORIZING_VALIDATION='Validating' +STRING_LOGIN_JETPACK_STEPS_AUTHORIZING_DONE='Connected' +STRING_LOGIN_JETPACK_STEPS_DONE='All done' +STRING_LOGIN_JETPACK_INSTALLATION_STEPS_SCREEN_TITLE='Installing Jetpack' +STRING_LOGIN_JETPACK_CONNECTION_STEPS_SCREEN_TITLE='Connecting Jetpack' +STRING_LOGIN_JETPACK_CONNECTION_STEPS_SCREEN_TITLE_DONE='Connected Jetpack' +STRING_LOGIN_JETPACK_INSTALLATION_STEPS_SCREEN_TITLE_DONE='Installed Jetpack' +STRING_LOGIN_JETPACK_STEPS_SCREEN_SUBTITLE_RE='Please\ wait\ while\ we\ connect\ your\ store\ .*\ with\ Jetpack\.' +STRING_LOGIN_JETPACK_STEPS_SCREEN_SUBTITLE_DONE_RE='Your\ store\ .*\ is\ now\ connected\ to\ Jetpack\.' +STRING_LOGIN_JETPACK_INSTALLATION_ERROR_CODE_TEMPLATE_RE='Error\ code\ .*' +STRING_LOGIN_JETPACK_INSTALLATION_ERROR='Error' +STRING_LOGIN_JETPACK_INSTALLATION_GO_TO_STORE_BUTTON='Go to store' +STRING_LOGIN_JETPACK_INSTALLATION_APPROVE_CONNECTION='Connect Jetpack' +STRING_LOGIN_JETPACK_INSTALLATION_ERROR_INSTALLING='Error installing Jetpack' +STRING_LOGIN_JETPACK_INSTALLATION_ERROR_ACTIVATING='Error activating Jetpack' +STRING_LOGIN_JETPACK_INSTALLATION_ERROR_AUTHORIZING='Error authorising connection to Jetpack' +STRING_LOGIN_JETPACK_INSTALLATION_ERROR_PLUGIN_PERMISSION_MESSAGE='You don’t have permission to manage plugins on this store' +STRING_LOGIN_JETPACK_INSTALLATION_ERROR_CONNECTION_MESSAGE='There was an error communicating with your site.' +STRING_LOGIN_JETPACK_INSTALLATION_ERROR_CONNECTION_SUGGESTION='Please connect Jetpack through your admin page on a browser or contact support.' +STRING_LOGIN_JETPACK_INSTALLATION_ERROR_GENERIC_MESSAGE='There was an error communicating with your site.' +STRING_LOGIN_JETPACK_INSTALLATION_ERROR_GENERIC_SUGGESTION='Please try again and contact support if this error continues.' +STRING_LOGIN_JETPACK_INSTALLATION_GET_SUPPORT='Get support' +STRING_LOGIN_JETPACK_INSTALLATION_RETRY_INSTALLING='Try installing again' +STRING_LOGIN_JETPACK_INSTALLATION_RETRY_ACTIVATING='Try activating again' +STRING_LOGIN_JETPACK_INSTALLATION_RETRY_AUTHORIZING='Try authorising again' +STRING_LOGIN_JETPACK_INSTALLATION_CANCEL='Cancel installation' +STRING_LOGIN_JETPACK_INSTALLATION_ERROR_FORBIDDEN_SUGGESTION='Please contact your administrator for help.' +STRING_LOGIN_JETPACK_INSTALLATION_ERROR_CONNECTION_PERMISSION_MESSAGE='You don’t have permission to connect to Jetpack on this store' +STRING_LOGIN_JETPACK_INSTALLATION_CONNECTION_DISMISSED='Jetpack is installed, but not connected.' +STRING_LOGIN_JETPACK_INSTALLATION_CONNECTION_DISMISSED_EXPLANATION='Try connecting again to access your store.' +STRING_LOGIN_JETPACK_INSTALLATION_CONTINUE_CONNECTION='Continue connection' +STRING_LOGIN_JETPACK_INSTALLATION_EXIT_WITHOUT_CONNECTION='Exit Without Connecting' +STRING_LOGIN_APPLICATION_PASSWORDS_UNAVAILABLE_RE='It\ looks\ like\ Application\ Passwords\ feature\ is\ disabled\ in\ your\ site\ .*\.\\n\ Please\ enable\ it\ to\ use\ the\ WooCommerce\ app\.' +STRING_LOGIN_JETPACK_CONNECTION_ENTER_WPCOM_EMAIL='Log in with your WordPress.com account to connect Jetpack' +STRING_LOGIN_JETPACK_INSTALLATION_ENTER_WPCOM_EMAIL='Log in with your WordPress.com account to install Jetpack' +STRING_LOGIN_JETPACK_CONNECTION_ENTER_WPCOM_PASSWORD='Enter the password of your WordPress.com account to connect to Jetpack' +STRING_LOGIN_JETPACK_INSTALLATION_ENTER_WPCOM_PASSWORD='Enter the password of your WordPress.com account to install Jetpack' +STRING_LOGIN_JETPACK_INSTALLATION_CONTINUE_MAGIC_LINK='Or continue using Magic Link' +STRING_LOGIN_JETPACK_INSTALLATION_MAGIC_LINK_FAILURE='An error occurred while fetching your website, please retry!' +STRING_LOGIN_SITE_CREDENTIALS_INVALID_RESPONSE='Login failed with an unexpected response from your site.' +STRING_LOGIN_SITE_CREDENTIALS_CUSTOM_LOGIN_URL='Unable to login because we cannot identify your store'"'"'s login URL' +STRING_LOGIN_SITE_CREDENTIALS_CUSTOM_ADMIN_URL='Unable to login because we cannot identify your store'"'"'s admin URL' +STRING_LOGIN_SITE_CREDENTIALS_HTTP_ERROR_RE='Login\ failed\ with\ status\ code\ .*' +STRING_LOGIN_SITE_CREDENTIALS_USE_WEB_AUTHORIZATION='Try again with WP Admin page' +STRING_LOGIN_SITE_CREDENTIALS_HTTP_BASIC_AUTH_ERROR='Basic Authentication is enabled on your site. Basic Authentication is unsupported in the WooCommerce mobile app. Please disable it, or contact support for troubleshooting.' +STRING_LOGIN_SITE_CREDENTIALS_FETCHING_SITE_FAILED='An error occurred while fetching your website' +STRING_LOGIN_SITE_CREDENTIALS_FETCHING_SITE='Fetching site…' +STRING_LOGIN_SITE_CREDENTIALS_WEB_AUTHORIZATION_CONNECTION_REJECTED='Unable to login because application password creation is not approved.' +STRING_LOGIN_APP_LOGIN_MALFORMED_LINK='We couldn'"'"'t process your app login request' +STRING_LOGIN_JETPACK_CONNECTION_CREATE_ACCOUNT='If you don'"'"'t have an account, we'"'"'ll use this email to create one.' +STRING_LOGIN_WPCOM_CONNECTION_CONSENT_RE='.*,\ you\ agree\ to\ our\ Terms\ of\ Service\ and\ to\ share\ details\ with\ WordPress\.com\.' +STRING_LOGIN_WPCOM_CONNECTION_CONSENT_JETPACK='By tapping the Connect Jetpack button' +STRING_LOGIN_WPCOM_CONNECTION_CONSENT_GENERIC='By continuing' +STRING_SITE_PICKER_SELECT_STORE_LIST_HEADER_WITH_HIDDEN_SITES_RE='Select\ store\ to\ connect\ \(.*\ hidden\)' +STRING_LOGIN_SITE_PICKER_ENTER_SITE_ADDRESS='Enter a site address' +STRING_LOGIN_SITE_PICKER_ADD_A_STORE='Connect another store' +STRING_SITE_PICKER_CREATE_NEW_STORE='Create a new store' +STRING_SITE_PICKER_CONNECT_EXISTING_STORE='Connect an existing store' +STRING_SITE_PICKER_UNKNOWN_BLOG_ERROR='We couldn'"'"'t access your store, so it'"'"'s been signed out. Please select a store to continue.' +STRING_SITE_PICKER_EDIT_STORE_LIST='Edit Stores' +STRING_SITE_PICKER_EDIT_STORE_LIST_TITLE='Visible Stores' +STRING_SITE_PICKER_EDIT_STORE_LIST_FOOTER='Unselected stores won'"'"'t be shown in the app site'"'"'s picker and won'"'"'t receive push notifications for new orders or product reviews.' +STRING_SITE_PICKER_EDIT_STORE_CURRENT_SITE_HEADER='Current Store' +STRING_SITE_PICKER_EDIT_STORE_CURRENT_SITE_FOOTER='Please switch to another store if you want to hide this one.' +STRING_SITE_PICKER_EDIT_STORE_LIST_HEADER='Other Stores' +STRING_SITE_PICKER_EDIT_STORE_LIST_ERROR_TITLE='There was an error when updating notification settings. Please try again' +STRING_DASHBOARD_STATS_EDIT_GRANULARITY_CONTENT_DESCRIPTION='Change date range button' +STRING_DASHBOARD_FILTER_MENU_CONTENT_DESCRIPTION='Open filter dropdown' +STRING_DASHBOARD_STATS_VISITORS='Visitors' +STRING_DASHBOARD_STATS_ORDERS='Orders' +STRING_DASHBOARD_STATS_REVENUE_TYPE_GROSS='Gross' +STRING_DASHBOARD_STATS_REVENUE_TYPE_NET='Net' +STRING_DASHBOARD_STATS_REVENUE_TYPE_TOTAL='Total' +STRING_DASHBOARD_STATS_PAID_ORDERS='Paid orders' +STRING_DASHBOARD_STATS_PLACED_ORDERS='Placed orders' +STRING_DASHBOARD_STATS_COMPLETED_ORDERS='Completed orders' +STRING_DASHBOARD_STATS_ORDER_DATE_TYPE_SHEET_TITLE='Order date type' +STRING_DASHBOARD_STATS_ORDER_DATE_TYPE_SHEET_DESCRIPTION='Choose which orders to include in your performance metrics for the selected time range.' +STRING_DASHBOARD_STATS_PAID_ORDERS_DESCRIPTION='Count orders on the date the order was paid.' +STRING_DASHBOARD_STATS_PLACED_ORDERS_DESCRIPTION='Count orders on the date they were placed or created.' +STRING_DASHBOARD_STATS_COMPLETED_ORDERS_DESCRIPTION='Count orders on the date they were marked as completed.' +STRING_DASHBOARD_STATS_ORDER_DATE_TYPE_SHEET_FOOTER='This is a store-wide setting, which also controls the “Date type” option in WooCommerce admin analytics settings.' +STRING_DASHBOARD_STATS_ORDER_DATE_TYPE_UPDATE_ERROR='Unable to update order type. Please try again.' +STRING_DASHBOARD_STATS_CONVERSION='Conversion' +STRING_DASHBOARD_STATE_NO_DATA='No revenue this period' +STRING_DASHBOARD_STATS_ERROR='Error fetching data' +STRING_DASHBOARD_STATS_ERROR_CONTENT_DESCRIPTION='Error image' +STRING_DASHBOARD_TOP_PERFORMERS_ITEMS_SOLD='Items Sold' +STRING_DASHBOARD_TOP_PERFORMERS_EMPTY='No activity this period' +STRING_DASHBOARD_TOP_PERFORMERS_NET_SALES_RE='Net\ sales:\ .*' +STRING_DASHBOARD_TOP_PERFORMERS_WCANALYTICS_INACTIVE_TITLE='Unable to load the top performers' +STRING_DASHBOARD_STATS_DELAYED_FOOTER='Stats may be up to 12 hours delayed' +STRING_DASHBOARD_STATS_INFO_CONTENT_DESCRIPTION='Analytics update settings' +STRING_DASHBOARD_SCHEDULED_IMPORT_SHEET_TITLE='Analytics updates' +STRING_DASHBOARD_SCHEDULED_IMPORT_SHEET_DESCRIPTION='Choose how WooCommerce processes analytics data updates. Learn more' +STRING_DASHBOARD_SCHEDULED_IMPORT_OPTION_SCHEDULED_TITLE='Scheduled' +STRING_DASHBOARD_SCHEDULED_IMPORT_OPTION_SCHEDULED_DESCRIPTION='Automatically updates analytics data every 12 hours. Recommended for high order volume stores.' +STRING_DASHBOARD_SCHEDULED_IMPORT_OPTION_IMMEDIATELY_TITLE='Immediately' +STRING_DASHBOARD_SCHEDULED_IMPORT_OPTION_IMMEDIATELY_DESCRIPTION='Updates analytics data as soon as new data becomes available.' +STRING_DASHBOARD_SCHEDULED_IMPORT_STORE_WIDE_NOTE='This is a store-wide setting, which also controls the "Updates" option in WooCommerce admin analytics settings.' +STRING_DASHBOARD_SCHEDULED_IMPORT_OPTION_SELECTED='Selected' +STRING_DASHBOARD_SCHEDULED_IMPORT_UPDATE_ERROR='Couldn'"'"'t update the setting. Please try again.' +STRING_STORE_CONNECTION_ERROR_DIALOG_TITLE='Your store can'"'"'t be reached' +STRING_STORE_CONNECTION_ERROR_DIALOG_BODY='We'"'"'re having trouble connecting to your store. This is usually a connection issue on your WordPress site, often caused by a security plugin, a recent plugin update, or a Jetpack connection that needs to be refreshed. The WooCommerce app can'"'"'t fix this from your phone.' +STRING_STORE_CONNECTION_ERROR_DIALOG_CONTACT_SUPPORT='Contact support' +STRING_DASHBOARD_ACTION_VIEW_ALL_ORDERS='View all orders' +STRING_DASHBOARD_ACTION_VIEW_ALL_MESSAGES='View all messages' +STRING_MY_STORE_STATS_PLUGIN_INACTIVE_TITLE='We can'"'"'t display your\n store'"'"'s analytics' +STRING_MY_STORE_CUSTOM_RANGE_CONTENT_DESCRIPTION='Add custom date range stats' +STRING_MY_STORE_CUSTOM_RANGE_GRANULARITY_LABEL_RE='.*\ interval' +STRING_MY_STORE_CUSTOM_RANGE_GRANULARITY_HOUR='Hourly' +STRING_MY_STORE_CUSTOM_RANGE_GRANULARITY_DAY='Daily' +STRING_MY_STORE_CUSTOM_RANGE_GRANULARITY_WEEK='Weekly' +STRING_MY_STORE_CUSTOM_RANGE_GRANULARITY_MONTH='Monthly' +STRING_MY_STORE_CUSTOM_RANGE_GRANULARITY_YEAR='Yearly' +STRING_MY_STORE_CUSTOM_RANGE_VISITORS_STATS_UNAVAILABLE_TITLE='Visitors and conversion data not available' +STRING_MY_STORE_CUSTOM_RANGE_VISITORS_STATS_UNAVAILABLE_MESSAGE='The stats feature does not support the display of visitors and conversions data for arbitrary date ranges.\n\nHowever, you can tap a value on the graph to see visitors and conversions for that specific range.' +STRING_MY_STORE_EDIT_SCREEN_WIDGETS='Customize' +STRING_DASHBOARD_AI_ASSISTANT_ENTRY_POINT_LABEL='Ask about your store…' +STRING_DASHBOARD_AI_ASSISTANT_ENTRY_POINT_CONTENT_DESCRIPTION='Open AI Assistant' +STRING_MY_STORE_WIDGET_PUSH_NOTIFICATIONS_TITLE='Never miss a new order' +STRING_MY_STORE_WIDGET_PUSH_NOTIFICATIONS_MENU_TITLE='Enable push notifications' +STRING_MY_STORE_WIDGET_PUSH_NOTIFICATIONS_DESCRIPTION='Enable push notifications to stay on top of new orders and reviews.' +STRING_MY_STORE_WIDGET_ONBOARDING_TITLE='Store setup' +STRING_MY_STORE_WIDGET_STATS_TITLE='Performance' +STRING_MY_STORE_WIDGET_TOP_PRODUCTS_TITLE='Top performers' +STRING_MY_STORE_WIDGET_BLAZE_TITLE='Blaze campaigns' +STRING_MY_STORE_WIDGET_ORDERS_TITLE='Most recent orders' +STRING_MY_STORE_WIDGET_REVIEWS_TITLE='Most recent reviews' +STRING_MY_STORE_WIDGET_COUPONS_TITLE='Most active coupons' +STRING_MY_STORE_WIDGET_PRODUCT_STOCK_TITLE='Stock' +STRING_MY_STORE_WIDGET_GOOGLE_ADS_TITLE='Google Ads campaigns' +STRING_MY_STORE_WIDGET_UNAVAILABLE='Unavailable' +STRING_MY_STORE_WIDGET_ONBOARDING_COMPLETED='Completed' +STRING_DYNAMIC_DASHBOARD_WIDGET_MENU_ITEM_HIDE_RE='Hide\ .*' +STRING_DYNAMIC_DASHBOARD_WIDGET_ERROR_TITLE='Unable to load data' +STRING_DYNAMIC_DASHBOARD_WIDGET_ERROR_DESCRIPTION='Try reloading this card. If the issue persists, please contact support.' +STRING_DASHBOARD_REVIEWS_CARD_HEADER_TITLE='Status' +STRING_DASHBOARD_REVIEWS_CARD_EMPTY_TITLE_FILTERED='No reviews found' +STRING_DASHBOARD_REVIEWS_CARD_EMPTY_MESSAGE_FILTERED='No reviews match the selected filter, please try changing the filter' +STRING_DASHBOARD_REVIEWS_CARD_VIEW_ALL_BUTTON='View all reviews' +STRING_DASHBOARD_COUPONS_CARD_HEADER_COUPONS='Coupons' +STRING_DASHBOARD_COUPONS_CARD_HEADER_USES='Uses' +STRING_DASHBOARD_COUPONS_VIEW_ALL_BUTTON='View all coupons' +STRING_DASHBOARD_COUPONS_CARD_EMPTY_VIEW_MESSAGE='No coupon usage during this period' +STRING_DASHBOARD_COUPONS_WCANALYTICS_INACTIVE_TITLE='Unable to load coupon usage report' +STRING_DASHBOARD_WCANALYTICS_INACTIVE_DESCRIPTION='Make sure you are running the latest version of WooCommerce on your site and that you have WooCommerce Analytics activated.' +STRING_DASHBOARD_WCANALYTICS_INACTIVE_CONTACT_US='Still need help? Contact us' +STRING_DASHBOARD_PRODUCT_STOCK_STATUS_HEADER_TITLE='Status' +STRING_DASHBOARD_PRODUCT_STOCK_LEVELS='Stock levels' +STRING_DASHBOARD_PRODUCT_STOCK_PRODUCTS='Products' +STRING_DASHBOARD_PRODUCT_STOCK_SALES_LAST_30_DAYS_RE='.*\ items\ sold\ in\ last\ 30\ days' +STRING_DASHBOARD_PRODUCT_STOCK_NO_SALES_LAST_30_DAYS='No items sold in the last 30 days' +STRING_DASHBOARD_PRODUCT_STOCK_WCANALYTICS_INACTIVE_TITLE='Unable to load product stock reports' +STRING_DASHBOARD_PRODUCT_STOCK_EMPTY_PRODUCTS='No products found for the selected stock status' +STRING_DASHBOARD_GOOGLE_ADS_CARD_NO_CAMPAIGN_HEADING='Drive sales and generate more traffic with Google Ads' +STRING_DASHBOARD_GOOGLE_ADS_CARD_NO_CAMPAIGN_DESCRIPTION='Promote your products across Google Search, Shopping, Youtube, Gmail, and more.' +STRING_DASHBOARD_GOOGLE_ADS_CARD_HAS_CAMPAIGN_HEADING='Paid campaign performance' +STRING_DASHBOARD_GOOGLE_ADS_CARD_HAS_CAMPAIGN_IMPRESSIONS='Impressions' +STRING_DASHBOARD_GOOGLE_ADS_CARD_HAS_CAMPAIGN_CLICKS='Clicks' +STRING_DASHBOARD_GOOGLE_ADS_CARD_CREATE_CAMPAIGN_BUTTON='Create Campaign' +STRING_DASHBOARD_GOOGLE_ADS_CARD_VIEW_ALL_CAMPAIGNS_BUTTON='View all campaigns' +STRING_DASHBOARD_NEW_WIDGETS_CARD_TITLE='Looking for more insights?' +STRING_DASHBOARD_NEW_WIDGETS_CARD_DESCRIPTION='Add new sections to customize your store management experience' +STRING_DASHBOARD_NEW_WIDGETS_CARD_BUTTON='Add new Sections' +STRING_ANALYTICS_SECTION_SEE_ALL='View all store analytics' +STRING_ANALYTICS_REVENUE_CARD_TITLE='Revenue' +STRING_ANALYTICS_REVENUE_NO_DATA='No revenue this period' +STRING_ANALYTICS_ORDERS_NO_DATA='No orders this period' +STRING_ANALYTICS_PRODUCTS_NO_DATA='No products this period' +STRING_ANALYTICS_GIFT_CARDS_NO_DATA='No gift cards this period' +STRING_ANALYTICS_GOOGLE_ADS_NO_DATA='No Programs this period' +STRING_ANALYTICS_SESSION_NO_DATA='No sessions this period' +STRING_ANALYTICS_SESSION_NO_AVAILABLE='Session data unavailable' +STRING_ANALYTICS_CUSTOM_LIST_SELECTION_BUTTON_DESCRIPTION='Filter selection' +STRING_ANALYTICS_SESSION_NO_AVAILABLE_DESCRIPTION='Session analytics rely on unique visitor counts not available for custom date ranges' +STRING_ANALYTICS_TOTAL_SALES_TITLE='Total sales' +STRING_ANALYTICS_SPEND_SUBTITLE_VALUE_RE='Spend:\ .*' +STRING_ANALYTICS_TOTAL_SALES_SUBTITLE_VALUE_RE='Total\ Sales:\ .*' +STRING_ANALYTICS_USED_TITLE='Used' +STRING_ANALYTICS_NET_SALES_TITLE='Net sales' +STRING_ANALYTICS_ORDERS_CARD_TITLE='Orders' +STRING_ANALYTICS_TOTAL_ORDERS_TITLE='Total Orders' +STRING_ANALYTICS_AVG_ORDERS_TITLE='Average Order Value' +STRING_ANALYTICS_PRODUCTS_LIST_ITEMS_SOLD='Items sold' +STRING_ANALYTICS_BUNDLES_LIST_ITEMS_SOLD='Bundles sold' +STRING_ANALYTICS_PRODUCTS_CARD_TITLE='Products' +STRING_ANALYTICS_PRODUCTS_LIST_HEADER_TITLE='Products' +STRING_ANALYTICS_BUNDLES_LIST_HEADER_TITLE='Bundles' +STRING_ANALYTICS_PRODUCTS_LIST_HEADER_SUBTITLE='Items sold' +STRING_ANALYTICS_BUNDLES_LIST_HEADER_SUBTITLE='Bundles sold' +STRING_ANALYTICS_PRODUCTS_LIST_ITEM_DESCRIPTION_RE='Net\ sales:\ .*' +STRING_ANALYTICS_SESSION_CARD_TITLE='Sessions' +STRING_ANALYTICS_BUNDLES_CARD_TITLE='Bundles' +STRING_ANALYTICS_GIFT_CARDS_CARD_TITLE='Gift Cards' +STRING_ANALYTICS_GOOGLE_ADS_CARD_TITLE='Google Campaigns' +STRING_ANALYTICS_GOOGLE_ADS_METRIC_CARD_TITLE='Metric' +STRING_ANALYTICS_GOOGLE_ADS_FILTER_TOTAL_SALES='Total Sales' +STRING_ANALYTICS_GOOGLE_ADS_FILTER_SPEND='Spend' +STRING_ANALYTICS_GOOGLE_ADS_FILTER_CONVERSION='Conversion' +STRING_ANALYTICS_GOOGLE_ADS_FILTER_IMPRESSIONS='Impressions' +STRING_ANALYTICS_GOOGLE_ADS_FILTER_CLICKS='Clicks' +STRING_ANALYTICS_GOOGLE_ADS_PROGRAMS_CARD_TITLE='Programs' +STRING_ANALYTICS_CONVERSION_SUBTITLE='Conversion Rate' +STRING_ANALYTICS_VISITORS_SUBTITLE='Visitors' +STRING_ANALYTICS_ITEM='item' +STRING_ANALYTICS_ITEMS='items' +STRING_ANALYTICS_LIST_ITEM_PRODUCTS_SOLD_RE='.*,\ .*,\ .*,\ .*\ sold' +STRING_ANALYTICS_GOOGLE_ADS_CTA_TITLE='Google Campaigns' +STRING_ANALYTICS_GOOGLE_ADS_CTA_DESCRIPTION='Drive sales and generate more traffic with Google Ads.' +STRING_ANALYTICS_GOOGLE_ADS_CTA_ACTION='Add paid campaign' +STRING_ANALYTICS_GOOGLE_ADS_CTA_WEB_VIEW_TITLE='Google for WooCommerce' +STRING_ANALYTICS_BANNER_TITLE='Enjoying analytics?' +STRING_ANALYTICS_BANNER_MESSAGE='Please rate your analytics experience' +STRING_ORDERLIST_ERROR_FETCH_GENERIC='Error fetching orders' +STRING_ORDER_LIST_CUSTOMER_FILTER_APPLIED='Customer filter applied' +STRING_ORDERLIST_SEARCH_HINT='Search orders' +STRING_ORDERLIST_SEARCH_HINT_ACTIVE_FILTERS='Search filtered orders' +STRING_ORDERLIST_LOADING='Looking up your orders…' +STRING_ORDERLIST_NO_FILTERS_TITLE='All Orders' +STRING_ORDER_CARD_TRANSITION_NAME_RE='order_card_.*' +STRING_ORDER_CARD_DETAIL_TRANSITION_NAME='order_card_detail' +STRING_ORDERLIST_MARK_COMPLETED='Mark\ncompleted' +STRING_ORDERLIST_MARK_COMPLETED_SUCCESS_RE='Order\ \#.*\ marked\ as\ completed' +STRING_ORDERLIST_UPDATING_ORDER_ERROR_RE='Error\ updating\ Order\ \#.*' +STRING_ORDERLIST_PARSING_ERROR_TITLE='We couldn'"'"'t load your data.' +STRING_ORDERLIST_PARSING_ERROR_MESSAGE='This could be related to a conflict with a plugin. Please try again later or reach out to us and we'"'"'ll be happy to assist you!' +STRING_ERROR_TROUBLESHOOTING='Troubleshooting' +STRING_ORDERLIST_TIMEOUT_ERROR_TITLE='Your site is taking a long time to respond' +STRING_ORDERLIST_TIMEOUT_ERROR_MESSAGE='Please try again later or reach out to us and we'"'"'ll be happy to assist you' +STRING_ORDERLIST_CONNECTIVITY_TOOL_TITLE='Troubleshoot Connection' +STRING_ORDERLIST_CONNECTIVITY_TOOL_SUBTITLE='Please wait while we attempt to identify your connection issue.' +STRING_ORDERLIST_CONNECTIVITY_TOOL_INTERNET_CHECK_TITLE='Internet connection' +STRING_ORDERLIST_CONNECTIVITY_TOOL_INTERNET_CHECK_SUGGESTION='It looks like you'"'"'re not connected to the internet.\n\nEnsure your Wi-Fi is turned on. If you'"'"'re using mobile data, make sure it'"'"'s enabled in your device settings.' +STRING_ORDERLIST_CONNECTIVITY_TOOL_WORDPRESS_CHECK_TITLE='Connecting to WordPress.com servers' +STRING_ORDERLIST_CONNECTIVITY_TOOL_WORDPRESS_CHECK_SUGGESTION='We can'"'"'t connect to WordPress.com right now.\n\nTry again in a few minutes, or contact our support team and we will happily assist you.' +STRING_ORDERLIST_CONNECTIVITY_TOOL_STORE_CHECK_TITLE='Connecting to your site' +STRING_ORDERLIST_CONNECTIVITY_TOOL_STORE_ORDERS_CHECK_TITLE='Fetching your site orders' +STRING_ORDERLIST_CONNECTIVITY_TOOL_STORE_PRODUCTS_CHECK_TITLE='Fetching products in your store' +STRING_ORDERLIST_CONNECTIVITY_TOOL_CONTACT_SUPPORT_ACTION='Contact Support' +STRING_ORDERLIST_CONNECTIVITY_TOOL_READ_MORE_ACTION='Read More' +STRING_ORDERLIST_CONNECTIVITY_TOOL_RETRY_ACTION='Retry connection' +STRING_ORDERLIST_CONNECTIVITY_TOOL_RETURN_ACTION='Return to the previous screen' +STRING_ORDERLIST_CONNECTIVITY_TOOL_TIMEOUT_ERROR_SUGGESTION='Your site seems to be taking too long to respond.\n\nContact your hosting provider for further assistance.' +STRING_ORDERLIST_CONNECTIVITY_TOOL_PARSING_ERROR_SUGGESTION='It seems we can'"'"'t work properly with your site'"'"'s response.\n\nBut don'"'"'t worry, our support team is here to help. Contact us and we will happily assist you.' +STRING_ORDERLIST_CONNECTIVITY_TOOL_JETPACK_ERROR_SUGGESTION='There seems to be a problem with your jetpack connection.\n\nBut don'"'"'t worry, our support team is here to help. Contact us and we will happily assist you.' +STRING_ORDERLIST_CONNECTIVITY_TOOL_GENERIC_ERROR_SUGGESTION='There seems to be a problem with your site.\n\nContact your hosting provider for further assistance.' +STRING_ORDERLIST_CONNECTIVITY_TOOL_SUMMARY_TITLE='No connection issues' +STRING_ORDERLIST_CONNECTIVITY_TOOL_SUMMARY_SUGGESTION='If your data still isn'"'"'t loading, contact our support team for assistance.' +STRING_CONNECTIVITY_TOOL_VIEW_TECHNICAL_DETAILS='View technical details' +STRING_CONNECTIVITY_TOOL_TECHNICAL_DETAILS_TITLE='Technical details' +STRING_CONNECTIVITY_TOOL_TECHNICAL_DETAILS_COPIED='Technical details have been copied to your clipboard.' +STRING_CONNECTIVITY_TOOL_COPY='Copy' +STRING_ORDERLIST_ORDER_TRASHED='Order trashed' +STRING_ORDERLIST_ORDER_TRASHED_ERROR='Error trashing order' +STRING_ORDERLIST_SELECTION_COUNT_RE='.*\ orders\ selected' +STRING_ORDERLIST_SELECTION_COUNT_SINGLE_RE='.*\ order\ selected' +STRING_ORDERLIST_SELECTION_MENU_UPDATE_STATUS='Update status' +STRING_ORDERLIST_BULK_UPDATE_STATUS_UPDATED='Status updated!' +STRING_ORDERLIST_BULK_UPDATE_MAXIMUM_REACHED_RE='Maximum\ selection\ count\ \(.*\)\ is\ reached\.' +STRING_ORDERLIST_BULK_UPDATE_RESULT_NO_ORDERS_UPDATED='No orders updated. Please try again.' +STRING_ORDERLIST_BULK_UPDATE_RESULT_ALL_FAILED='Failed to update orders. Please try again.' +STRING_ORDERLIST_BULK_UPDATE_RESULT_PARTIAL_SUCCESS_RE='.*\ order\(s\)\ updated,\ and\ .*\ order\(s\)\ failed\ to\ update\.\ Please\ try\ again\.' +STRING_SIMPLE_PAYMENTS_TITLE='Simple payment' +STRING_SIMPLE_PAYMENTS_UPDATE_ERROR='Unable to update simple payment order' +STRING_SIMPLE_PAYMENTS_EDIT_EMAIL_HINT='Enter email' +STRING_SIMPLE_PAYMENTS_CUSTOM_AMOUNT='Custom amount' +STRING_SIMPLE_PAYMENTS_CHARGE_TAXES='Charge taxes' +STRING_SIMPLE_PAYMENTS_TAKE_PAYMENT_BUTTON_RE='Take\ payment\ \(.*\)' +STRING_SIMPLE_PAYMENTS_TAX_MESSAGE='Taxes are automatically calculated based on your store address' +STRING_SIMPLE_PAYMENTS_CHOOSE_METHOD='Choose your payment method' +STRING_SIMPLE_PAYMENTS_SHARE_PAYMENT_LINK='Share Payment Link' +STRING_SIMPLE_PAYMENTS_SHARE_PAYMENT_DIALOG_TITLE_RE='Checkout\ \-\ .*' +STRING_CASH_PAYMENTS_TAKE_PAYMENT_TITLE_RE='Take\ payment\ \(.*\)' +STRING_CASH_PAYMENTS_CASH_RECEIVED='Cash received' +STRING_CASH_PAYMENTS_CHANGE_DUE='Change due' +STRING_CASH_PAYMENTS_RECORD_TRANSACTION_DETAILS='Record transaction details in order note' +STRING_CASH_PAYMENTS_MARK_ORDER_AS_COMPLETE='Mark Order as Complete' +STRING_CASH_PAYMENTS_ORDER_NOTE_TEXT_RE='The\ order\ was\ paid\ by\ cash\.\ Customer\ paid\ .*\.\ The\ change\ due\ was\ .*\.' +STRING_CASH_PAYMENTS_ORDER_NOTE_ADDING_ERROR='Error adding order note' +STRING_CUSTOM_AMOUNTS_ENTER_AMOUNT='Amount' +STRING_CUSTOM_AMOUNTS_NAME='Name' +STRING_CUSTOM_AMOUNTS_ADD_CUSTOM_AMOUNT='Add Custom Amount' +STRING_CUSTOM_AMOUNTS_DELETE_CUSTOM_AMOUNT='Delete Custom Amount' +STRING_CUSTOM_AMOUNTS_ADD_CUSTOM_NAME_HINT='Enter Custom name' +STRING_CUSTOM_AMOUNTS_TAX_LABEL='Charge Taxes' +STRING_CUSTOM_AMOUNTS_PERCENTAGE_LABEL_RE='Percentage\ of\ order\ total\ \(.*\)' +STRING_CUSTOM_AMOUNTS_BOTTOM_SHEET_HEADING='How do you want to add your custom amount?' +STRING_CUSTOM_AMOUNTS_BOTTOM_SHEET_FIXED_AMOUNT_OPTION='A fixed amount' +STRING_CUSTOM_AMOUNTS_BOTTOM_SHEET_PERCENTAGE_AMOUNT_OPTION='A percentage of the order total' +STRING_CUSTOM_AMOUNTS_PERCENTAGE_HINT='0' +STRING_CUSTOM_AMOUNTS_PERCENTAGE_SYMBOL='%' +STRING_CUSTOM_AMOUNTS_PERCENTAGE_INVALID_VALUE='Invalid value' +STRING_CUSTOM_AMOUNTS_SAVE_CHANGES='Save Changes' +STRING_TAX_NAME_WITH_TAX_PERCENT_RE='.*\ \(.*%%\)' +STRING_ORDERLIST_CREATE_ORDER_BUTTON_DESCRIPTION='Create order' +STRING_ORDER_LIST_BARCODE_SCANNING_SCANNING_FAILED='Scanning failed. Please try again later' +STRING_ORDER_CREATION_FRAGMENT_TITLE='New order' +STRING_ORDER_CREATION_TABLET_MODE_FRAGMENT_TITLE='Order Summary' +STRING_ORDER_CREATION_PRICE_AFTER_DISCOUNT='Price after discount' +STRING_ORDER_CREATION_PRODUCTS_ORDER_COUNT='Order count' +STRING_ORDER_CREATION_CUSTOMER='Customer' +STRING_ORDER_CREATION_CUSTOMER_EDIT_CONTENT_DESCRIPTION='Edit customer details' +STRING_ORDER_CREATION_CUSTOMER_NOTE='Customer note' +STRING_ORDER_CREATION_CUSTOMER_NOTE_EDIT_CONTENT_DESCRIPTION='Edit customer note' +STRING_ORDER_CREATION_ADD_CUSTOMER='Add customer details' +STRING_ORDER_CREATION_ADD_CUSTOMER_CONTENT_DESCRIPTION='add customer' +STRING_ORDER_CREATION_ADD_CUSTOMER_NOTE='Add note' +STRING_ORDER_CREATION_ADD_PRODUCTS='Add products' +STRING_ORDER_CREATION_ADD_CUSTOM_AMOUNTS='Add custom amount' +STRING_ORDER_CREATION_ADD_PRODUCT_VIA_BARCODE_SCANNING='Add products via scanner' +STRING_ORDER_CREATION_SCAN_PRODUCTS='Scan products' +STRING_ORDER_CREATION_SET_TAX_RATE='Set New Tax Rate' +STRING_ORDER_CREATION_EDIT_TAX_RATE='Edit Tax Rate Setting' +STRING_ORDER_CREATION_ADD_FEE='Add fee' +STRING_ORDER_CREATION_ADD_COUPON='Add coupon' +STRING_ORDER_CREATION_ADD_GIFT_CARD='Add gift card' +STRING_ORDER_CREATION_SELECT_COUPON='Select a coupon' +STRING_ORDER_CREATION_COUPON_DISCOUNT_VALUE_RE='\-.*' +STRING_ORDER_CREATION_COUPON_BUTTON='Coupons' +STRING_ORDER_CREATION_COUPONS_TITLE='Coupons applied' +STRING_ORDER_CREATION_INCREASE_ITEM_AMOUNT_CONTENT_DESCRIPTION='Increase product quantity' +STRING_ORDER_CREATION_DECREASE_ITEM_AMOUNT_CONTENT_DESCRIPTION='Decrease product quantity' +STRING_COUPON_SELECTOR_EMPTY_LIST_BUTTON='Go to Coupons' +STRING_COUPON_SELECTOR_EMPTY_LIST_MESSAGE='You haven'"'"'t created any coupons yet. Create a coupon to apply it to this order.' +STRING_COUPON_SELECTOR_EMPTY_LIST_TITLE='Everyone loves a deal' +STRING_ORDER_CREATION_REMOVE_COUPON='Remove coupon from order' +STRING_ORDER_CREATION_REMOVE_THIS_COUPON='Remove coupon' +STRING_ORDER_CREATION_CUSTOMER_DETAILS='Customer details' +STRING_CUSTOMER_DETAIL_GUEST_CUSTOMER='Guest' +STRING_ORDER_CREATION_NEW_CUSTOMER_ADD_DIFFERENT_SHIPPING_ADDRESS='Add a different shipping address' +STRING_ORDER_CREATION_ADD_DISCOUNT='Add discount' +STRING_ORDER_CREATION_DISCOUNT_AMOUNT_LABEL='Calculated Amount' +STRING_ORDER_CREATION_DISCOUNT_PERCENTAGE_LABEL='Calculated Percentage' +STRING_ORDER_CREATION_PRICE_AFTER_DISCOUNT_LABEL='Price after discount' +STRING_ORDER_CREATION_REMOVE_DISCOUNT='Remove discount' +STRING_ORDER_CREATION_DISCOUNT_TOO_BIG_ERROR='Discount cannot be greater than the price' +STRING_ORDER_CREATION_DISCOUNT_AMOUNT_WITH_CURRENCY_RE='Amount\ \(.*\)' +STRING_ORDER_CREATION_DISCOUNTS_TOTAL='Discounts Total' +STRING_ORDER_CREATION_DISCOUNTS_TOTAL_VALUE_RE='\-\ .*' +STRING_ORDER_CREATION_REMOVE_PRODUCT='Remove product from order' +STRING_ORDER_CREATION_PAYMENT_PRODUCTS='Products' +STRING_ORDER_CREATION_PAYMENT_ORDER_TOTAL='Order Total' +STRING_ORDER_CREATION_LOADING_DIALOG_TITLE='Creating your order' +STRING_ORDER_CREATION_LOADING_DIALOG_MESSAGE='Please wait…' +STRING_ORDER_CREATION_FAILURE_SNACKBAR='Order creation failed' +STRING_ORDER_CREATION_SUCCESS_SNACKBAR='Order created' +STRING_ORDER_CREATION_PAYMENT_TAX_LABEL='Taxes' +STRING_ORDER_CREATION_SHIPPING_NAME='Name' +STRING_ORDER_CREATION_SHIPPING_TITLE_ADD='Add Shipping' +STRING_ORDER_CREATION_SHIPPING_TITLE_EDIT='Edit Shipping' +STRING_ORDER_CREATION_SHIPPING_METHODS_TITLE='Method' +STRING_ORDER_CREATION_SHIPPING_METHODS_ERROR='Error while fetching your shipping methods. Please try again' +STRING_ORDER_CREATION_SHIPPING_ADD='Add Shipping' +STRING_ORDER_CREATION_SHIPPING_EDIT='Edit Shipping' +STRING_ORDER_CREATION_ADD_SHIPPING='Add shipping' +STRING_ORDER_CREATION_ADD_SHIPPING_METHOD='Method' +STRING_ORDER_CREATION_ADD_SHIPPING_AMOUNT='Amount' +STRING_ORDER_CREATION_ADD_SHIPPING_NAME='Name' +STRING_ORDER_CREATION_ADD_SHIPPING_NAME_HINT='Shipping' +STRING_ORDER_CREATION_REMOVE_SHIPPING='Remove shipping from order' +STRING_ORDER_CREATION_ADD_FEE_REMOVAL_HINT='Remove fee from order' +STRING_ORDER_CREATION_FEE_PERCENTAGE_HINT='Percentage (%)' +STRING_ORDER_CREATION_FEE_PERCENTAGE_TOGGLE_TEXT='Calculate as percentage' +STRING_ORDER_CREATION_FEE_PERCENTAGE_CALCULATED_AMOUNT_RE='Calculated\ amount:\ .*' +STRING_ORDER_CREATION_CUSTOMER_SEARCH_EMPTY='No customers found' +STRING_ORDER_CREATION_CUSTOMER_SEARCH_EMPTY_ON_OLD_VERSION_WCPAY='Search for an existing customer or' +STRING_ORDER_CREATION_CUSTOMER_SEARCH_HINT='Search for customers' +STRING_ORDER_CREATION_CUSTOMER_SEARCH_OLD_WC_HINT='Search for customers by' +STRING_ORDER_CREATION_CUSTOMER_SEARCH_EMAIL='Email' +STRING_ORDER_CREATION_CUSTOMER_SEARCH_NAME='Name' +STRING_ORDER_CREATION_CUSTOMER_SEARCH_USERNAME='Username' +STRING_ORDER_CREATION_CUSTOMER_SEARCH_EMPTY_NAME='No name' +STRING_ORDER_CREATION_CUSTOMER_SEARCH_EMPTY_EMAIL='No email address' +STRING_ORDER_CREATION_CUSTOMER_SEARCH_EMPTY_ADD_DETAILS_MANUALLY='Add details manually' +STRING_ORDER_CREATION_CUSTOMER_SEARCH_EMPTY_ADD_DETAILS_MANUALLY_WITH_EMAIL='Add details manually using email' +STRING_ORDER_CREATION_BARCODE_SCANNING_UNABLE_TO_ADD_PRODUCT_RE='Product\ with\ SKU\ .*\ not\ found\.\ Unable\ to\ add\ to\ the\ order' +STRING_ORDER_CREATION_BARCODE_SCANNING_SCANNING_FAILED='Scanning failed. Please try again later' +STRING_ORDER_CREATION_BARCODE_SCANNING_UNABLE_TO_ADD_VARIABLE_PRODUCT='You cannot add variable product directly. Please select a specific variation' +STRING_ORDER_CREATION_BARCODE_SCANNING_UNABLE_TO_ADD_DRAFT_PRODUCT='You cannot add products that are not published' +STRING_ORDER_CREATION_BARCODE_SCANNING_UNABLE_TO_ADD_PRODUCT_WITH_INVALID_PRICE='You cannot add products with no price specified' +STRING_ORDER_CREATION_TAX_BASED_ON_STORE_ADDRESS='Calculated on store address' +STRING_ORDER_CREATION_TAX_BASED_ON_BILLING_ADDRESS='Calculated on billing address' +STRING_ORDER_CREATION_TAX_BASED_ON_SHIPPING_ADDRESS='Calculated on shipping address' +STRING_ORDER_CREATION_COLLAPSE_EXPAND_PRODUCT_CARD_CONTENT_DESCRIPTION='Collapse/expand product card' +STRING_ORDER_CREATION_GIFT_CARD_TEXT_FIELD_HINT='Enter code' +STRING_ORDER_CREATION_GIFT_CARD_TEXT_ERROR='The code should be in XXXX-XXXX-XXXX-XXXX format' +STRING_ORDER_CREATION_COLLECT_PAYMENT_BUTTON='Collect Payment' +STRING_ORDER_CREATION_RECALCULATE_BUTTON='Recalculate' +STRING_ORDER_CREATION_EXPAND_COLLAPSE_ORDER_TOTALS='Expand collapse order totals' +STRING_ORDER_CREATION_PAYMENT_SHIPPING_TAX_LABEL='Shipping Tax' +STRING_CUSTOMER_PICKER_GUEST_CUSTOMER_NOT_ALLOWED_MESSAGE='This user is a guest, and guests can'"'"'t be used for filtering orders.' +STRING_ORDER_CREATION_SHIPPING_FEEDBACK_TITLE='Shipping added!' +STRING_ORDER_CREATION_SHIPPING_FEEDBACK_MESSAGE='Does Woo make shipping easy?' +STRING_ORDER_CREATION_FEEDBACK_ACTION='Share your feedback' +STRING_BARCODE_SCANNING_TITLE='Scan Barcode' +STRING_BARCODE_SCANNING_ALERT_DIALOG_TITLE='Grant Camera Permission' +STRING_BARCODE_SCANNING_ALERT_DIALOG_RATIONALE_MESSAGE='Camera permission is required in order to scan the barcode' +STRING_BARCODE_SCANNING_ALERT_DIALOG_PERMANENTLY_DENIED_MESSAGE='You have permanently denied Camera permission. It is required in order to scan the barcode. Please enable it from the app settings' +STRING_BARCODE_SCANNING_ALERT_DIALOG_RATIONALE_CTA_LABEL='Grant' +STRING_BARCODE_SCANNING_ALERT_DIALOG_DISMISS_LABEL='Cancel' +STRING_BARCODE_SCANNING_ALERT_DIALOG_PERMANENTLY_DENIED_CTA_LABEL='Go to settings' +STRING_BARCODE_SCANNING_SCAN_PRODUCT_BARCODE_LABEL='Scan Product Barcode' +STRING_ORDER_EDITING_NON_EDITABLE_TITLE='Parts of this order are not currently editable' +STRING_ORDER_EDITING_NON_EDITABLE_MESSAGE='To edit Products or Payment Details, change the status to Pending Payment.' +STRING_ORDER_EDITING_CURRENCY_MISMATCH_TITLE='This order can'"'"'t be edited in the app' +STRING_ORDER_EDITING_CURRENCY_MISMATCH_MESSAGE_RE='Sorry,\ you\ can\ only\ edit\ this\ order\ on\ the\ web,\ as\ it\ uses\ .*,\ and\ your\ site'"'"'s\ currency\ is\ .*\.' +STRING_ORDER_EDITING_LOCKED_CONTENT_DESCRIPTION='locked' +STRING_ORDER_EDITING_BARCODE_CONTENT_DESCRIPTION='Scan barcode' +STRING_ORDER_EDITING_ADD_CONTENT_DESCRIPTION='Add product' +STRING_ORDER_SYNC_FAILED='Unable to save changes' +STRING_ORDER_SYNC_COUPON_REMOVED='Coupon could not be applied and was removed from the order' +STRING_ORDERFILTERS_SHOW_ORDERS_BUTTON='Show Orders' +STRING_ORDERFILTERS_DEFAULT_FILTER_VALUE='All' +STRING_ORDERFILTERS_ORDER_STATUS_FILTER='Order Status' +STRING_ORDERFILTERS_DATE_RANGE_FILTER='Date Range' +STRING_ORDERFILTERS_PRODUCT_FILTER='Product' +STRING_ORDERFILTERS_CUSTOMER_FILTER='Customer' +STRING_ORDERFILTERS_SALES_CHANNEL_FILTER='Sales Channel' +STRING_ORDERFILTERS_SELECTED_FILTER_FALLBACK_DISPLAY_VALUE_RE='Id:\ .*' +STRING_ORDERFILTERS_FILTER_OPTION_ITEM_SELECTED='Selected filter option' +STRING_ORDERFILTERS_FILTER_ORDER_STATUS_OPTIONS_TITLE='Order Status' +STRING_ORDERFILTERS_FILTER_DATE_RANGE_OPTIONS_TITLE='Date Range' +STRING_ORDERFILTERS_FILTER_CARD_TITLE_ALL_ORDERS='All orders' +STRING_ORDERFILTERS_FILTER_CARD_TITLE_FILTERED_ORDERS='Filtered orders' +STRING_ORDERFILTERS_FILTERS_DEFAULT_TITLE='Filters' +STRING_ORDERFILTERS_FILTERS_COUNT_TITLE_RE='Filters\ \(.*\)' +STRING_ORDERFILTERS_ORDER_STATUS_WITH_COUNT_FILTER_OPTION_RE='.*\ \(.*\)' +STRING_ORDERFILTERS_DATE_RANGE_FILTER_TODAY='Today' +STRING_ORDERFILTERS_DATE_RANGE_FILTER_LAST_TWO_DAYS='Last 2 Days' +STRING_ORDERFILTERS_DATE_RANGE_FILTER_LAST_7_DAYS='Last 7 days' +STRING_ORDERFILTERS_DATE_RANGE_FILTER_LAST_30_DAYS='Last 30 days' +STRING_ORDERFILTERS_SALES_CHANNEL_FILTER_WEB_CHECKOUT='Web checkout' +STRING_ORDERFILTERS_SALES_CHANNEL_FILTER_WP_ADMIN='WP-admin' +STRING_ORDERFILTERS_DATE_RANGE_FILTER_CUSTOM_RANGE='Custom Range' +STRING_ORDERFILTERS_DATE_RANGE_PICKER_TITLE='Select dates' +STRING_ORDERDETAIL_CUSTOMER_HEADER='CUSTOMER' +STRING_ORDERDETAIL_CUSTOMER_NOTE_RE='\\u0022.*\\u0022' +STRING_ORDERDETAIL_CUSTOMER_NAME_DEFAULT='Guest' +STRING_ORDERDETAIL_ORDERSTATUS_ORDERNUM_RE='Order\ \#.*' +STRING_ORDERDETAIL_SHIPPING_METHOD='Shipping method' +STRING_ORDERDETAIL_SHIPPING_DETAILS='Shipping details' +STRING_ORDERDETAIL_BILLING_DETAILS='Billing details' +STRING_ORDERDETAIL_EMAIL_CONTENTDESC='email customer' +STRING_ORDERDETAIL_VIEW_CUSTOMER_ORDERS='View customer orders' +STRING_ORDERDETAIL_CALL_OR_MESSAGE_CONTENTDESC='Call or message customer' +STRING_ORDERDETAIL_CALL_CUSTOMER='Call' +STRING_ORDERDETAIL_MESSAGE_CUSTOMER='Message' +STRING_ORDERDETAIL_MESSAGE_CUSTOMER_USING_WHATSAPP='Contact using WhatsApp' +STRING_ORDERDETAIL_MESSAGE_CUSTOMER_USING_TELEGRAM='Contact using Telegram' +STRING_ORDERDETAIL_PRODUCT_LINEITEM_ATTRIBUTES_RE='.*.*\ x\ .*' +STRING_ORDERDETAIL_PRODUCT_LINEITEM_SKU_VALUE_RE='SKU:\ .*' +STRING_ORDERDETAIL_PRODUCT_LINEITEM_VIEW_ADDONS_ACTION='View Add-ons' +STRING_ORDERDETAIL_PRODUCT_IMAGE_CONTENTDESC='Product Image' +STRING_ORDERDETAIL_PRODUCT='Product' +STRING_ORDERDETAIL_PRODUCT_MULTIPLE='Products' +STRING_ORDERDETAIL_PRODUCT_UPPERCASE='PRODUCT' +STRING_ORDERDETAIL_PRODUCT_MULTIPLE_UPPERCASE='PRODUCTS' +STRING_ORDERDETAIL_PRODUCT_QTY='QTY' +STRING_ORDERDETAIL_PAYMENT_SUMMARY_COMPLETED_RE='.*\ via\ .*' +STRING_ORDERDETAIL_PAYMENT_SUMMARY_ONHOLD_RE='Awaiting\ payment\ via\ .*' +STRING_ORDERDETAIL_PAYMENT_SUMMARY_ONHOLD_PLAIN='Awaiting payment' +STRING_ORDERDETAIL_PAYMENT_PAID_BY_CUSTOMER='Paid' +STRING_ORDERDETAIL_REFUNDED='Refunded' +STRING_ORDERDETAIL_REFUNDED_LINE_WITH_INFO_RE='Refunded:\ .*' +STRING_ORDERDETAIL_REFUNDED_PRODUCTS='Refunded products' +STRING_ORDERDETAIL_REFUND_DETAIL_RE='.*\ via\ .*' +STRING_ORDERDETAIL_NET='Net Payment' +STRING_ORDERDETAIL_DISCOUNT_ITEMS_RE='\(.*\)' +STRING_ORDERDETAIL_PAYMENT_FEES='Fees' +STRING_ORDERDETAIL_CUSTOMER_PROVIDED_NOTE='Customer provided note' +STRING_ORDERDETAIL_NOTE_HINT='Compose an order note' +STRING_ORDERDETAIL_NOTE_PRIVATE='Private' +STRING_ORDERDETAIL_NOTE_PUBLIC='To customer' +STRING_ORDERDETAIL_NOTE_SYSTEM='System' +STRING_ORDERDETAIL_ORDER_NOTES_UPPERCASE='ORDER NOTES' +STRING_ORDERDETAIL_CUSTOMER_NOTE_HINT='Edit a customer order note' +STRING_ORDERDETAIL_SHOW_BILLING='Show Billing' +STRING_ORDERDETAIL_HIDE_BILLING='Hide Billing' +STRING_ORDERDETAIL_ADD_NOTE='Add a note' +STRING_ORDERDETAIL_ADDNOTE_CONTENTDESC='Add an order note' +STRING_ORDERDETAIL_TRACK_SHIPMENT='Track shipment' +STRING_ORDERDETAIL_COPY_TRACKING_NUMBER='Copy tracking number' +STRING_ORDERDETAIL_DELETE_TRACKING='Delete tracking' +STRING_ORDERDETAIL_SHIPPING_NOTICE='This order is using extensions to calculate shipping. The shipping methods shown might be incomplete.' +STRING_ORDERDETAIL_ISSUE_REFUND_BUTTON='Issue refund' +STRING_ORDERDETAIL_COLLECT_PAYMENT_BUTTON='Collect Payment' +STRING_ORDERDETAIL_SEE_RECEIPT_BUTTON='See receipt' +STRING_ORDERDETAIL_PRINTING_INSTRUCTIONS_BUTTON='Learn more about printing receipts with your device' +STRING_ORDERDETAIL_PRODUCTS_RECREATE_SHIPPING_LABEL_MENU='Create new shipping label' +STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_HEADER_RE='Package\ .*' +STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_MENU='Refund shipping label' +STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_SHIPFROM='Ship from' +STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_SHIPTO='Ship to' +STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_PACKAGE_INFO='Package details' +STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_CARRIER='Shipping carriers and rates' +STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_PAYMENT='Payment method' +STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_PAYMENT_TYPE='Credit card' +STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_VIEW_PURCHASED_SHIPPING_LABEL='View purchased shipping label' +STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_SHOW_SHIPPING='Show shipment details' +STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_HIDE_SHIPPING='Hide shipment details' +STRING_ORDERDETAIL_SHIPPING_LABEL_CARRIER_INFO_RE='.*\\n.*' +STRING_ORDERDETAIL_SHIPPING_LABEL_REFUND_TITLE_RE='.*\ label\ refund\ requested' +STRING_ORDERDETAIL_SHIPPING_LABEL_REFUND_SUBTITLE_RE='.*\ \\u2022\ .*' +STRING_ORDERDETAIL_SHIPPING_LABEL_REQUEST_REFUND='Request a refund' +STRING_ORDERDETAIL_SHIPPING_LABEL_PRINT_CUSTOMS_FORM='Print customs form' +STRING_ORDERDETAIL_SHIPPING_LABEL_UNPACKAGED_PRODUCTS_HEADER='Remaining products' +STRING_ORDERDETAIL_SHIPPING_LABEL_PRINT='Print shipping label' +STRING_ORDERDETAIL_SHIPPING_LABEL_CREATE_SHIPPING_LABEL='Create shipping label' +STRING_ORDERDETAIL_SHIPPING_LABEL_NOTICE='Learn more about creating labels with your mobile device' +STRING_ORDERDETAIL_SHIPPING_LABEL_SHIPMENT_HEADER_RE='Shipment\ .*' +STRING_ORDERDETAIL_SHIPPING_LABEL_SHIPMENT_ITEMS_ONE_RE='.*\ item' +STRING_ORDERDETAIL_SHIPPING_LABEL_SHIPMENT_ITEMS_MULTIPLE_RE='.*\ items' +STRING_ORDERDETAIL_SHIPPING_LABEL_REFUNDED='You have successfully submitted a request for refund. You can purchase a new label.' +STRING_ORDERDETAIL_CUSTOM_FIELDS='Custom fields' +STRING_ORDERDETAIL_AI_CREATE_THANK_YOU_NOTE_BUTTON='✨Create thank-you note' +STRING_RECEIPT_PREVIEW_TOOLBAR_TITLE='Receipt Preview' +STRING_RECEIPT_PREVIEW_PRINT_MENU_ITEM='Print' +STRING_RECEIPT_PREVIEW_SEND_MENU_ITEM='Send' +STRING_ORDER_MARK_COMPLETE='Mark order complete' +STRING_ORDER_FULFILL_COMPLETED='🎉 Order completed!' +STRING_ORDER_STATUS_UPDATED='Order status updated' +STRING_ORDER_ERROR_FETCH_NOTES_GENERIC='Error fetching notes' +STRING_ORDER_ERROR_UPDATE_GENERAL='Error changing order' +STRING_ORDER_ERROR_UPDATE_EMPTY_MAIL='Unable to update address with empty email. Make sure you are running the latest version of WooCommerce.' +STRING_ORDER_ERROR_FETCH_GENERIC='Error fetching order' +STRING_ORDER_SHIPMENT_TRACKING='Tracking' +STRING_ORDER_SHIPMENT_TRACKING_NUMBER='Tracking Number' +STRING_ORDER_SHIPMENT_TRACKING_NUMBER_CLIPBOARD='Tracking number copied to the clipboard' +STRING_ORDER_DETAIL_SHIPMENT_TRACKING_BUTTON_CONTENTDESC='Track or delete shipment tracking' +STRING_ORDER_SHIPMENT_TRACKING_SECTION_CD='Shipment tracking' +STRING_ORDER_SHIPMENT_TRACKING_COPY_TO_CLIPBOARD='Copy tracking number to clipboard' +STRING_ORDER_SHIPMENT_TRACKING_ADD_BUTTON='Add tracking' +STRING_ORDER_SHIPMENT_TRACKING_DELETE_SNACKBAR_MSG='Deleted shipment tracking' +STRING_ORDER_SHIPMENT_TRACKING_DELETE_ERROR='Error deleting tracking' +STRING_ORDER_SHIPMENT_TRACKING_DELETE_SUCCESS='Tracking deleted' +STRING_ORDER_SHIPMENT_TRACKING_BARCODE_SCANNING_FAILED='Scanning failed. Please try again later' +STRING_ORDERED_ADD_ONS_DETAILS_INFO_NOTICE='If renaming an add-on in your web dashboard, please note that previous orders will no longer show that add-on within the app.' +STRING_ORDERED_ADD_ONS_WIP_TITLE='View add-ons from your device!' +STRING_ORDERED_ADD_ONS_WIP_MESSAGE='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.' +STRING_ORDERED_ADD_ONS_LOADING_FAILED_DIALOG_TITLE='Something went wrong' +STRING_ORDERED_ADD_ONS_LOADING_FAILED_DIALOG_MESSAGE='Sorry, we couldn'"'"'t load the Order Add-ons right now' +STRING_ORDERED_ADD_ONS_LOADING_FAILED_DIALOG_OK_ACTION='OK' +STRING_ORDER_DETAIL_EDIT_ADDRESS_DETAILS_FIRST_NAME='First name' +STRING_ORDER_DETAIL_EDIT_ADDRESS_DETAILS_LAST_NAME='Last name' +STRING_ORDER_DETAIL_EDIT_ADDRESS_DETAILS_EMAIL='Email' +STRING_ORDER_DETAIL_EDIT_ADDRESS_DETAILS_PHONE='Phone' +STRING_ORDER_DETAIL_EDIT_ADDRESS_COMPANY='Company' +STRING_ORDER_DETAIL_EDIT_ADDRESS_LINE1='Address 1' +STRING_ORDER_DETAIL_EDIT_ADDRESS_LINE2='Address 2' +STRING_ORDER_DETAIL_EDIT_ADDRESS_CITY='City' +STRING_ORDER_DETAIL_EDIT_ADDRESS_ZIP='Postal Code' +STRING_ORDER_DETAIL_ADDRESS_SECTION='Address' +STRING_ORDER_DETAIL_SHIPPING_ADDRESS_SECTION='Shipping Address' +STRING_ORDER_DETAIL_BILLING_ADDRESS_SECTION='Billing Address' +STRING_ORDER_DETAIL_ADD_CUSTOMER_NOTE='Add customer note' +STRING_ORDER_DETAIL_ADD_SHIPPING_ADDRESS='Add shipping address' +STRING_ORDER_DETAIL_ADD_BILLING_ADDRESS='Add billing address' +STRING_ORDERDETAIL_EMPTY_ADDRESS='No address specified' +STRING_ORDER_DETAIL_USE_AS_BILLING_ADDRESS='Use as Billing Address' +STRING_ORDER_DETAIL_USE_AS_SHIPPING_ADDRESS='Use as Shipping Address' +STRING_ORDER_DETAIL_PAYMENT_HEADER='PAYMENT TOTALS' +STRING_ORDER_DETAIL_CUSTOM_AMOUNTS_HEADER='CUSTOM AMOUNTS' +STRING_ORDER_DETAIL_SHIPPING_HEADER='SHIPPING' +STRING_ORDER_DETAIL_ATTRIBUTION_HEADER='Order attribution' +STRING_ORDER_DETAIL_ATTRIBUTION_ORIGIN='Origin' +STRING_ORDER_DETAIL_ATTRIBUTION_ORGANIC_ORIGIN_RE='Organic:\ .*' +STRING_ORDER_DETAIL_ATTRIBUTION_REFERRAL_ORIGIN_RE='Referral:\ .*' +STRING_ORDER_DETAIL_ATTRIBUTION_UTM_ORIGIN_RE='Source:\ .*' +STRING_ORDER_DETAIL_ATTRIBUTION_DIRECT_ORIGIN='Direct' +STRING_ORDER_DETAIL_ATTRIBUTION_ADMIN_ORIGIN='Web admin' +STRING_ORDER_DETAIL_ATTRIBUTION_MOBILE_ORIGIN='Mobile App' +STRING_ORDER_DETAIL_ATTRIBUTION_UNKNOWN_ORIGIN='Unknown' +STRING_ORDER_DETAIL_ATTRIBUTION_SOURCE_TYPE='Source type' +STRING_ORDER_DETAIL_ATTRIBUTION_SOURCE='Source' +STRING_ORDER_DETAIL_ATTRIBUTION_MEDIUM='Medium' +STRING_ORDER_DETAIL_ATTRIBUTION_CAMPAIGN='Medium' +STRING_ORDER_DETAIL_ATTRIBUTION_DEVICE_TYPE='Device type' +STRING_ORDER_DETAIL_ATTRIBUTION_SESSION_PAGE_VIEWS='Session page views' +STRING_ORDER_DETAIL_MOVE_TO_TRASH='Move to trash' +STRING_ORDER_DETAIL_TRASH_ORDER_DIALOG_MESSAGE='Do you want to move this order to the trash?' +STRING_SHIPPING_LABEL_REFUND_MESSAGE='You can request a refund for a shipping label that has not been used to ship a package. It will take at least 14 days to process.' +STRING_SHIPPING_LABEL_REFUND_PURCHASE_DATE_TITLE='Purchase date' +STRING_SHIPPING_LABEL_REFUND_AMOUNT_TITLE='Amount eligible for refund' +STRING_SHIPPING_LABEL_REFUND_BUTTON_RE='Refund\ label\ \(\-.*\)' +STRING_SHIPPING_LABEL_REFUND_SUCCESS='Refund request was successfully submitted' +STRING_SHIPPING_LABEL_REFUND_PROGRESS_MESSAGE='Your refund is being processed. Please wait…' +STRING_SHIPPING_LABEL_REFUND_EXPIRED='Labels older than 30 days cannot be refunded' +STRING_ORDER_FULFILL_TITLE='Review order' +STRING_ORDER_FULFILL_EMAIL_INFO='If you’ve enabled this setting, the customer will receive a confirmation email once the order is completed' +STRING_SHIPPING_LABEL_PRINT_ERROR_MESSAGE='If there was a printing error when you purchased the label, you can print it again.' +STRING_SHIPPING_LABEL_PRINT_DISCLAIMER='If you already used the label on a package, printing and using it again is a violation of our terms of service.' +STRING_SHIPPING_LABEL_PAPER_SIZE='Paper size' +STRING_SHIPPING_LABEL_PAPER_SIZE_OPTIONS_TITLE='Choose paper size' +STRING_SHIPPING_LABEL_PRINT_BUTTON='Print shipping label' +STRING_SHIPPING_LABEL_PRINT_MULTIPLE_BUTTON='Print shipping labels' +STRING_SHIPPING_LABEL_PAPER_SIZE_OPTIONS_INFO='See label layout and paper size options' +STRING_SHIPPING_LABEL_PRINT_INFO='Don'"'"'t know how to print with your mobile device?' +STRING_SHIPPING_LABEL_PRINT_PURCHASE_SUCCESS='Shipping label purchased!' +STRING_SHIPPING_LABEL_PRINT_MULTIPLE_PURCHASE_SUCCESS='Shipping labels purchased!' +STRING_SHIPPING_LABEL_PRINT_SAVE_FOR_LATER='Save for later' +STRING_SHIPPING_LABEL_PRINT_SCREEN_TITLE='Print shipping label' +STRING_SHIPPING_LABEL_PRINT_MULTIPLE_SCREEN_TITLE='Print shipping labels' +STRING_SHIPPING_LABEL_PREVIEW_ERROR='Error previewing shipping label' +STRING_SHIPPING_LABEL_PREVIEW_PDF_APP_MISSING='Unable to preview shipping label. Please install a PDF viewer app and try again.' +STRING_SHIPPING_LABEL_PAPER_SIZE_LEGAL='Legal (8.5 x 14 in)' +STRING_SHIPPING_LABEL_PAPER_SIZE_A4='A4 (210 x 297mm)' +STRING_SHIPPING_LABEL_PAPER_SIZE_LETTER='Letter (8.5 x 11 in)' +STRING_SHIPPING_LABEL_PAPER_SIZE_LABEL='Label (4 x 6 in)' +STRING_PRINT_RECEIPT_LABEL_INFO_STEP_1='Make sure the Print Service Plugin for your printer is installed.' +STRING_PRINT_RECEIPT_LABEL_INFO_STEP_2='Enable bluetooth or Wifi connection on your printer.' +STRING_PRINT_RECEIPT_LABEL_INFO_STEP_3='When selecting "Print receipt" after accepting payment, replace "Save as PDF" with "All printers", and search for new printer.' +STRING_PRINT_RECEIPT_LABEL_INFO_STEP_4='Pair and connect the printer to your mobile when prompted.' +STRING_PRINT_RECEIPT_LABEL_INFO_STEP_5='Adjust paper size as needed, and select "Print" when ready to print the receipt.' +STRING_PRINT_RECEIPT_LABEL_INFO_STEP_6='If printing is not available, you can always save your receipt as PDF and send it by email to print it from another device.' +STRING_PRINT_RECEIPT_LABEL_INFO_STEP_7='If you are experiencing issues printing from your device, contact customer support for your printer.' +STRING_PRINT_SHIPPING_LABEL_INFO_TITLE='Print with your device' +STRING_PRINT_SHIPPING_LABEL_FORMAT_OPTIONS_TITLE='Label format options' +STRING_PRINT_SHIPPING_LABEL_INFO_STEP_1='Make sure your printer and your device are connected to the same WiFi networks' +STRING_PRINT_SHIPPING_LABEL_INFO_STEP_2='After selecting "Print shipping label", you may have to select and add a printer if you haven'"'"'t printed from this device before.' +STRING_PRINT_SHIPPING_LABEL_INFO_STEP_3='You can select your device'"'"'s default print service or install your printer'"'"'s brand app (this should appear as a recommended option)' +STRING_PRINT_SHIPPING_LABEL_INFO_STEP_4='You might have to configure WiFi printing directly on the printer itself. Make sure the printer firmware is updated and see your printer documentation for instructions.' +STRING_PRINT_SHIPPING_LABEL_INFO_STEP_5='If you are still experiencing issues printing from your device, you can save your label as PDF and send it by email to print it from another device.' +STRING_SHIPPING_LABEL_REPRINT_EXPIRED_MESSAGE='Label images older than 180 days are deleted by our technology partners for general security and data privacy concerns.' +STRING_SHIPPING_LABEL_MORE_INFORMATION_TITLE='WooCommerce Shipping' +STRING_SHIPPING_LABEL_MORE_INFORMATION_HEADING='Save time and money by fulfilling with WooCommerce Shipping' +STRING_SHIPPING_LABEL_MORE_INFORMATION_MESSAGE='Cut the post office line by printing shipping labels at home with your mobile device at discounted rates!' +STRING_SHIPPING_LABEL_MORE_INFORMATION_LINK='Learn more' +STRING_SHIPPING_LABEL_CREATE_TITLE='Create shipping label' +STRING_SHIPPING_LABEL_CREATE_PACKAGING_DETAILS='Packaging details' +STRING_SHIPPING_LABEL_CREATE_PACKAGING_DETAILS_DESCRIPTION='Select the type of packaging you\’d like to ship your items in' +STRING_SHIPPING_LABEL_CREATE_CUSTOMS='Customs' +STRING_SHIPPING_LABEL_CREATE_CUSTOMS_DESCRIPTION='Fill out customs form' +STRING_SHIPPING_LABEL_CREATE_CUSTOMS_DONE='Customs form completed' +STRING_SHIPPING_LABEL_CREATE_CARRIER_DESCRIPTION='Select your shipping carrier and rates' +STRING_SHIPPING_LABEL_CREATE_PAYMENT_DESCRIPTION='Add a new credit card' +STRING_SHIPPING_LABEL_CREATE_ORDER_SUMMARY='Shipping label order summary' +STRING_SHIPPING_LABEL_CREATE_PRICE_SUBTOTAL='Subtotal' +STRING_SHIPPING_LABEL_CREATE_PRICE_WOO_DISCOUNT='WooCommerce Services discount' +STRING_SHIPPING_LABEL_CREATE_PRICE_WOO_DISCOUNT_CONTENT_DESCRIPTION='Learn more about WooCommerce Services discount' +STRING_SHIPPING_LABEL_CREATE_PRICE_TOTAL='Order total' +STRING_SHIPPING_LABEL_CREATE_MARK_ORDER_COMPLETE='Mark this order as complete and notify the customer' +STRING_SHIPPING_LABEL_CREATE_PURCHASE='Purchase shipping label' +STRING_SHIPPING_LABEL_CREATE_PURCHASE_PROGRESS_TITLE='Purchasing label' +STRING_SHIPPING_LABEL_CREATE_PURCHASE_PROGRESS_MESSAGE='Please wait…' +STRING_SHIPPING_LABEL_CREATE_PURCHASE_ERROR='Error purchasing the labels' +STRING_SHIPPING_LABEL_CREATE_PURCHASE_FULFILL_ERROR='Couldn'"'"'t mark the order as complete' +STRING_SHIPPING_LABEL_SELECTED_PAYMENT_DESCRIPTION_RE='Credit\ card\ ending\ in\ .*' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_NAME='Name' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_COMPANY='Company' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_PHONE='Phone' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_LINE1='Address line 1' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_LINE2='Address line 2' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_CITY='City' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_STATE='State' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_STATE_SEARCH_HINT='Filter state' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_ZIP='Zip / Postal Code' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_COUNTRY='Country' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_COUNTRY_SEARCH_HINT='Filter countries' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_USE_ADDRESS_AS_IS='Use address as entered' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_VALIDATION_PROGRESS_TITLE='Address validation in progress' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_LOADING_PROGRESS_TITLE='Loading address data' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_PROGRESS_MESSAGE='Please wait…' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_VALIDATION_ERROR='Address validation failed' +STRING_SHIPPING_LABEL_EDIT_ORIGIN_ADDRESS_ERROR_WARNING='We were unable to automatically verify the origin address. View it on Google Maps to make sure the address is correct.' +STRING_SHIPPING_LABEL_EDIT_ADDRESS_ERROR_WARNING='We were unable to automatically verify the shipping address. View it on Google Maps or try contacting the customer to make sure the address is correct.' +STRING_SHIPPING_LABEL_ERROR_ADDRESS_NOT_FOUND='Address was not found' +STRING_SHIPPING_LABEL_ERROR_ADDRESS_HOUSE_NUMBER_MISSING='House number missing' +STRING_SHIPPING_LABEL_ERROR_ADDRESS_INVALID_STREET='Invalid street' +STRING_SHIPPING_LABEL_VALIDATION_ERROR_TEMPLATE_RE='We\ were\ unable\ to\ automatically\ verify\ the\ shipping\ address:\ .*' +STRING_SHIPPING_LABEL_VALIDATION_CONTACT_CUSTOMER='Contact Customer' +STRING_SHIPPING_LABEL_VALIDATION_VIEW_MAP='Find on Map' +STRING_SHIPPING_LABEL_ADDRESS_SUGGESTION_USE_SELECTED_ADDRESS='Use selected address' +STRING_SHIPPING_LABEL_ADDRESS_SUGGESTION_EDIT_SELECTED_ADDRESS='Edit selected address' +STRING_SHIPPING_LABEL_ADDRESS_SUGGESTION_BANNER='We have slightly modified the address entered. If correct, please use the suggested address to ensure accurate delivery.' +STRING_SHIPPING_LABEL_ADDRESS_SUGGESTION_ENTERED_ADDRESS='Entered address' +STRING_SHIPPING_LABEL_ADDRESS_SUGGESTION_SUGGESTED_ADDRESS='Suggested address' +STRING_SHIPPING_LABEL_ADDRESS_PHONE_REQUIRED='A phone number is required' +STRING_SHIPPING_LABEL_ORIGIN_ADDRESS_PHONE_INVALID='Customs forms require a 10-digit phone number' +STRING_SHIPPING_LABEL_DESTINATION_ADDRESS_PHONE_INVALID='Please enter a valid phone number' +STRING_SHIPPING_LABEL_ADDRESS_DATA_INVALID_SNACKBAR_MESSAGE='Certain required fields are blank.' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_ITEMS_SECTION_TITLE='Items to fulfill' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_MOVE_ITEM='Move' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_SECTION_TITLE='Package details' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_SELECTED_PACKAGE_HINT='Package selected' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_WEIGHT_HINT_RE='Total\ package\ weight\ \(.*\)' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_WEIGHT_INFO='Includes package weight' +STRING_SHIPPING_LABEL_PACKAGES_LOADING_ERROR='Unable to load package definitions' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_ITEMS_COUNT_ONE_RE='.*\ item' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_ITEMS_COUNT_MANY_RE='.*\ items' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_INDIVIDUAL_PACKAGE_SUBTITLE='Individually Shipped Item' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_INDIVIDUAL_PACKAGE_DIMENSIONS='Item Dimensions' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_INDIVIDUAL_PACKAGE_TITLE='Original packaging' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_INDIVIDUAL_PACKAGE_DIMENSIONS_ERROR='Package dimensions must be greater than zero. Please update your item’s dimensions in the Shipping section of your product page to continue.' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_HAZMAT_CONTENT_CHECKBOX_TITLE='Contains Hazardous Materials' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_HAZMAT_CONTENT_DESCRIPTION='Potentially hazardous material includes items such as batteries, dry ice, flammable liquids, aerosols, ammunition, fireworks, nail polish, perfume, paint, solvents, and more. Hazardous items must ship in separate packages.' +STRING_SHIPPING_LABEL_USPS_HAZMAT_INSTRUCTIONS_RE='Learn\ how\ to\ securely\ package,\ label,\ and\ ship\ HAZMAT\ through\ USPS®\ at\ .*' +STRING_SHIPPING_LABEL_USPS_INSTRUCTIONS_LINK_TEXT='www.usps.com/hazmat.' +STRING_SHIPPING_LABEL_USPS_SEARCH_TOOL_RE='Determine\ your\ product'"'"'s\ mailability\ using\ the\ .*' +STRING_SHIPPING_LABEL_USPS_SEARCH_TOOL_LINK_TEXT='USPS HAZMAT Search Tool.' +STRING_SHIPPING_LABEL_HAZMAT_CONTENT_DHL_INSTRUCTIONS_RE='WooCommerce\ Shipping\ does\ not\ currently\ support\ HAZMAT\ shipments\ through\ .*' +STRING_SHIPPING_LABEL_HAZMAT_CONTENT_DHL_INSTRUCTIONS_LINK_TEXT='DHL Express.' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_HAZMAT_SELECTION_TITLE='Hazardous material category' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_HAZMAT_SELECT_CATEGORY_ACTION='Select a category' +STRING_SHIPPING_LABEL_ITEMS_COUNT_PLACEHOLDER='Items count' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_TITLE_TEMPLATE_RE='Package\ .*' +STRING_SHIPPING_LABEL_PACKAGE_SELECTOR_TITLE='Selected Package' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_WEIGHT_ERROR='Invalid weight' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_FETCH_PRODUCTS_ERROR='Can'"'"'t fetch products' +STRING_SHIPPING_LABEL_PACKAGES_CUSTOM_SECTION_TITLE='Custom packages' +STRING_SHIPPING_LABEL_SINGLE_PACKAGE_TOTAL_WEIGHT_RE='Total\ package\ weight:\ .*\ .*' +STRING_SHIPPING_LABEL_MULTI_PACKAGES_ITEMS_COUNT_RE='.*\ items\ in\ .*\ packages' +STRING_SHIPPING_LABEL_MULTI_PACKAGES_TOTAL_WEIGHT_RE='Total\ packages\ weight:\ .*\ .*' +STRING_SHIPPING_LABEL_CREATE_NEW_PACKAGE_BUTTON='Create new package' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_AIR_ELIGIBLE_ETHANOL='Air Eligible Ethanol Package - (authorized fragrance and hand sanitizer shipments)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_1='Class 1 – Toy Propellant/Safety Fuse Package' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_3='Class 3 - Package (Hand sanitizer, rubbing alcohol, ethanol base products, flammable liquids etc.)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_4='Class 4 - Package (Flammable solids)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_5='Class 5 - Package (Oxidizers)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_6='Class 6 - Package (Poisonous materials)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_7='Class 7 – Radioactive Materials Package (e.g., smoke detectors, minerals, gun sights, etc.)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_8_CORROSIVE='Class 8 – Corrosive Materials Package - Air Eligible Corrosive Materials (certain cleaning or tree/weed killing compounds, etc.)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_8_WET_BATTERY='Class 8 – Nonspillable Wet Battery Package - Sealed lead acid batteries' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_9_NEW_LITHIUM_INDIVIDUAL='Class 9 - Lithium Battery Marked – Ground Only Package - New Individual or spare lithium batteries (marked UN3480 or UN3090)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_9_USED_LITHIUM='Class 9 - Lithium Battery – Returns Package - Used electronic devices containing or packaged with lithium batteries (markings required)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_9_NEW_LITHIUM_DEVICE='Class 9 - Lithium batteries, marked package - New electronic devices packaged with lithium batteries (marked UN3481 or UN3091)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_9_DRY_ICE='Class 9 – Dry Ice Package (limited to 5 lbs. if shipped via Air)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_9_UNMARKED_LITHIUM='Class 9 – Lithium batteries, unmarked package - New electronic devices installed or packaged with lithium batteries (no marking)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_CLASS_9_MAGNETIZED='Class 9 – Magnetized Materials Package' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_DIVISION_4_1='Division 4.1 – Mailable flammable solids and Safety Matches Package - Safety/strike on box matches, book matches, mailable flammable solids' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_DIVISION_5_1='Division 5.1 – Oxidizers Package - Hydrogen peroxide (8 to 20%% concentration)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_DIVISION_5_2='Division 5.2 – Organic Peroxides Package' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_DIVISION_6_1='Division 6.1 – Toxic Materials Package (with an LD50 of 50 mg/kg or less) - (pesticides, herbicides, etc.)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_DIVISION_6_2='Division 6.2 - Hazardous Materials - Biological Materials (e.g., lab test kits, authorized COVID test kit returns)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_EXCEPTED_QUANTITY_PROVISION='Excepted Quantity Provision Package (e.g., small volumes of flammable liquids, corrosive, toxic or environmentally hazardous materials - marking required)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_GROUND_ONLY='Ground Only Hazardous Materials (For items that are not listed, but are restricted to surface only)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_ID8000='ID8000 Consumer Commodity Package - Air Eligible ID8000 Consumer Commodity (Non-flammableaerosols, Flammable combustible liquids, Toxic Substance, Miscellaneious hazardous materials)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_LIGHTERS='Lighters Package - Authorized Lighters' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_LIMITED_QUANTITY='LTD QTY Ground Package - Aerosols, spray disinfectants, spray paint, hair spray, propane, butane, cleaning products, etc. - Fragrances, nail polish, nail polish remover, solvents, hand sanitizer, rubbing alcohol, ethanol base products, etc. - Other limited quantity surface materials (cosmetics, cleaning products, paints, etc.)' +STRING_SHIPPING_LABEL_HAZMAT_OPTION_SMALL_QUANTITY_PROVISION='Small Quantity Provision Package (markings required)' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_ITEMS_WEIGHT_PRICE_RE='.*  ·  .*' +STRING_SHIPPING_LABEL_PACKAGE_DETAILS_ITEMS_EXPAND_CONTENT_DESCRIPTION='Collapse/expand items card' +STRING_SHIPPING_LABEL_HAZMAT_TITLE='Are you shipping dangerous goods or hazardous materials?' +STRING_SHIPPING_LABEL_PACKAGE_TITLE='Select a Package' +STRING_SHIPPING_LABEL_SELECT_PACKAGE_BUTTON='Select a Package' +STRING_SHIPPING_LABEL_SELECT_PACKAGE_TITLE='Select a package to get shipping rates' +STRING_SHIPPING_LABEL_PACKAGE_DEFAULT_NAME='Custom Package' +STRING_SHIPPING_LABEL_PACKAGE_SELECTED_TITLE='Package' +STRING_SHIPPING_LABEL_PACKAGE_SELECTED_DESCRIPTION='Change package selection' +STRING_SHIPPING_LABEL_SELECT_PACKAGE_DESCRIPTION='Enter your package'"'"'s dimensions or pick a carrier package option to see the available shipping rates.' +STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_TITLE='Shipment details' +STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_ORDER_DETAILS='Order details' +STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_SHIPMENT_COST='Shipment cost' +STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_SHIPMENT_COST_BASE_FEE_RE='.*\ \(base\ fee\)' +STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_MARK_ORDER_COMPLETE='After purchasing a label, mark this order as complete and notify the customer.' +STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_PURCHASE_LABEL_RE='Purchase\ Label\ ·\ .*' +STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_PURCHASE_LABEL_DISABLED='Purchase Label' +STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_PAYMENT_METHOD='Payment method' +STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_ADD_PAYMENT_METHOD='Add payment method' +STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_EDIT_PAYMENT_METHOD='Edit payment method' +STRING_SHIPPING_LABEL_SHIPPING_RATES_SORT_OPTION_CHEAPEST='Cheapest' +STRING_SHIPPING_LABEL_SHIPPING_RATES_SORT_OPTION_FASTEST='Fastest' +STRING_SHIPPING_LABEL_SHIPPING_SERVICE_TITLE='Shipping service' +STRING_SHIPPING_LABEL_PURCHASED_SUCCESS_TITLE='Your shipping label is ready to print' +STRING_SHIPPING_LABEL_PURCHASED_SUCCESS_MESSAGE='From here you can print the shipping label again or change the paper size of the label.' +STRING_SHIPPING_LABEL_PURCHASED_IN_PROGRESS_TITLE='Your shipping label is being processed' +STRING_SHIPPING_LABEL_PURCHASED_IN_PROGRESS_MESSAGE='Once it'"'"'s finished, you will be able to print the shipping label with a selected paper size' +STRING_SHIPPING_LABEL_PURCHASED_FAILURE_TITLE='We couldn'"'"'t process your shipping label' +STRING_SHIPPING_LABEL_PURCHASED_FAILURE_MESSAGE='Please try again later' +STRING_SHIPPING_LABEL_PURCHASED_NOTE='Note: Reusing a printed label is a violation of our terms of service and may result in criminal charges.' +STRING_SHIPPING_LABEL_PURCHASED_LEARN_HOW_TO_PRINT='Learn how to print from your mobile device' +STRING_SHIPPING_LABEL_PURCHASED_TRACK_SHIPMENT='Track shipment' +STRING_SHIPPING_LABEL_PURCHASED_SCHEDULE_PICK_UP='Schedule pickup' +STRING_SHIPPING_LABEL_PURCHASED_REQUEST_REFUND='Request refund' +STRING_SHIPPING_LABEL_TOTAL_SHIPMENT_WEIGHT='Total shipment weight (with package)' +STRING_SHIPPING_LABEL_PURCHASED_PICKUP_ERROR='We currently do not support Pick ups for this carrier' +STRING_SHIPPING_LABEL_PURCHASED_TRACKING_ERROR='We currently do not support Tracking for this carrier' +STRING_SHIPPING_LABEL_PURCHASED_PRINT_ERROR='Something went wrong with this Shipping Label, try again later' +STRING_SHIPPING_LABEL_SELECT_ORIGIN_DEFAULT_ADDRESS_RE='.*\ \(default\)' +STRING_SHIPPING_LABEL_SELECT_ORIGIN_ADDRESS='Address' +STRING_SHIPPING_LABELS_CUSTOMS_TITLE='Customs' +STRING_SHIPPING_LABELS_CUSTOMS_MISSING_INFO_BADGE='Missing info' +STRING_SHIPPING_LABELS_CUSTOMS_COMPLETED_BADGE='Completed' +STRING_SHIPPING_LABEL_CREATE_PACKAGE_TITLE='Add new package' +STRING_SHIPPING_LABEL_CREATE_PACKAGE_FIELD_INFO='Set up the package you'"'"'ll be using to ship your products. We'"'"'ll save it for future orders.' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_TYPE='Package type' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_TYPE_BOX='Box' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_TYPE_ENVELOPE='Envelope' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_NAME='Package name' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_LENGTH_RE='Length\ \(.*\)' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_WIDTH_RE='Width\ \(.*\)' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_HEIGHT_RE='Height\ \(.*\)' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_EMPTY_WEIGHT_RE='Empty\ package\ weight\ \(.*\)' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_EMPTY_WEIGHT_HELPER='Weight of empty package' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_EMPTY_HINT='This field is required.' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_INVALID_HINT='Invalid value.' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_SAVING_PROGRESS_TITLE='Creating new package' +STRING_SHIPPING_LABEL_CREATE_PACKAGE_SAVING_PROGRESS_MESSAGE='Please wait…' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_API_FAILURE_RE='Package\ creation\ failed:\ .*' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_API_UNKNOWN_FAILURE='Package creation failed: unknown API issue.' +STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_SUCCESS_MESSAGE_RE='".*"\ saved' +STRING_SHIPPING_LABEL_CREATE_SERVICE_PACKAGE_NOTHING_SELECTED='Select a package to activate.' +STRING_SHIPPING_LABEL_ACTIVATE_SERVICE_PACKAGE_SAVING_PROGRESS_TITLE='Activating package' +STRING_SHIPPING_LABEL_ACTIVATE_SERVICE_PACKAGE_EMPTY_TITLE='All available packages have been activated' +STRING_SHIPPING_LABEL_PAYMENTS_SELECTED_PAYMENT_METHOD='Payment method selected' +STRING_SHIPPING_LABEL_PAYMENTS_ADD_CREDIT_CARD='Add another credit card' +STRING_SHIPPING_LABEL_PAYMENTS_ADD_FIRST_CREDIT_CARD='Add a credit card' +STRING_SHIPPING_LABEL_PAYMENTS_TYPE_DIGITS_RE='.*\*\*\*\*.*' +STRING_SHIPPING_LABEL_PAYMENTS_ACCOUNT_INFO_RE='Credit\ cards\ are\ retrieved\ from\ the\ following\ WordPress\.com\ account:\ .*\ <.*>' +STRING_SHIPPING_LABEL_PAYMENTS_EMAIL_RECEIPTS_CHECKBOX_RE='Email\ the\ label\ purchase\ receipts\ to\ .*\ \(.*\)\ at\ .*' +STRING_SHIPPING_LABEL_PAYMENTS_EXPIRATION_DATE_RE='Expire\ .*' +STRING_SHIPPING_LABEL_PAYMENTS_SAVING_DIALOG_TITLE='Saving your settings' +STRING_SHIPPING_LABEL_PAYMENTS_SAVING_DIALOG_MESSAGE='Please wait…' +STRING_SHIPPING_LABEL_PAYMENTS_SAVING_ERROR='Error while saving your settings' +STRING_SHIPPING_LABEL_PAYMENT_METHOD_ADDED='Payment method added' +STRING_SHIPPING_LABEL_SHIPPING_CARRIERS_TITLE='Carriers and rates' +STRING_SHIPPING_LABEL_SHIPPING_CARRIER_RATES_UNAVAILABLE_TITLE='No shipping rates available' +STRING_SHIPPING_LABEL_SHIPPING_CARRIER_RATES_UNAVAILABLE_MESSAGE='Please double check your package dimensions and weight or try using a different package in Package Details' +STRING_SHIPPING_LABEL_SHIPPING_CARRIER_RATES_DELIVERY_ESTIMATE_ONE_RE='.*\ business\ day' +STRING_SHIPPING_LABEL_SHIPPING_CARRIER_RATES_DELIVERY_ESTIMATE_MANY_RE='.*\ business\ days' +STRING_SHIPPING_LABEL_SHIPPING_CARRIER_RATES_GENERIC_ERROR='There was an error loading shipping options' +STRING_SHIPPING_LABEL_WOO_DISCOUNT_BOTTOMSHEET_TITLE='What is WooCommerce Services discount?' +STRING_SHIPPING_LABEL_WOO_DISCOUNT_BOTTOMSHEET_MESSAGE='When purchasing shipping labels with WooCommerce, you get 5% to 40% off compared to post office rates.' +STRING_SHIPPING_LABEL_SHIPPING_CARRIER_FLAT_FEE_BANNER_MESSAGE_RE='Customer\ paid\ a\ .*\ of\ .*\ for\ shipping' +STRING_SHIPPING_LABEL_SHIPPING_CARRIER_SHIPPING_METHOD_BANNER_MESSAGE_RE='Your\ customer\ selected\ .*' +STRING_SHIPPING_LABEL_RATE_OPTION_SIGNATURE_REQUIRED_RE='Signature\ required\ \(.*\)' +STRING_SHIPPING_LABEL_RATE_OPTION_ADULT_SIGNATURE_REQUIRED_RE='Adult\ signature\ required\ \(.*\)' +STRING_SHIPPING_LABEL_RATE_INCLUDED_OPTIONS_RE='Includes\ .*' +STRING_SHIPPING_LABEL_RATE_INCLUDED_OPTIONS_USPS_TRACKING='USPS tracking' +STRING_SHIPPING_LABEL_RATE_INCLUDED_OPTIONS_TRACKING='tracking' +STRING_SHIPPING_LABEL_RATE_INSURANCE_UP_TO_RE='up\ to\ .*' +STRING_SHIPPING_LABEL_RATE_INCLUDED_OPTIONS_INSURANCE_RE='Insurance\ \(.*\)' +STRING_SHIPPING_LABEL_RATE_INCLUDED_OPTIONS_FREE_PICKUP='Eligible for free pickup' +STRING_SHIPPING_LABEL_RATE_INCLUDED_OPTIONS_SIGNATURE_REQUIRED_FREE='Eligible for free signature requirement' +STRING_SHIPPING_LABEL_SELECTED_RATES_DESCRIPTION_RE='.*\ rates\ selected' +STRING_SHIPPING_LABEL_SELECTED_RATES_TOTAL_DESCRIPTION_RE='.*\ total' +STRING_SHIPPING_LABEL_CUSTOMS_RETURN_TO_SENDER='Return to sender if package is unabled to be delivered' +STRING_SHIPPING_LABEL_CUSTOMS_CONTENTS_TYPE_HINT='Contents type' +STRING_SHIPPING_LABEL_CUSTOMS_RESTRICTION_TYPE_HINT='Restriction type' +STRING_SHIPPING_LABEL_CUSTOMS_CONTENTS_TYPE_OTHER_HINT='Contents details' +STRING_SHIPPING_LABEL_CUSTOMS_RESTRICTION_TYPE_OTHER_HINT='Restriction details' +STRING_SHIPPING_LABEL_CUSTOMS_ITN_INVALID_FORMAT='Invalid format' +STRING_SHIPPING_LABEL_CUSTOMS_ITN_REQUIRED_ITEMS_OVER_2500='ITN is required for shipping items valued over $2,500 per tariff number' +STRING_SHIPPING_LABEL_CUSTOMS_ITN_REQUIRED_COUNTRY_RE='ITN\ is\ required\ for\ shipments\ to\ .*\.' +STRING_SHIPPING_LABEL_CUSTOMS_PACKAGE_CONTENT='Package content' +STRING_SHIPPING_LABEL_CUSTOMS_ITEM_DESCRIPTION_HINT='Description' +STRING_SHIPPING_LABEL_CUSTOMS_HS_TARIFF_HINT='HS Tariff number (Optional)' +STRING_SHIPPING_LABEL_CUSTOMS_HS_TARIFF_INVALID_FORMAT='The tariff number must be 6 digits long' +STRING_SHIPPING_LABEL_CUSTOMS_ORIGIN_COUNTRY_HINT='Origin country' +STRING_SHIPPING_LABEL_CUSTOMS_ORIGIN_COUNTRY_EXPLANATION='Country where the product was manufactured or assembled' +STRING_SHIPPING_LABEL_CUSTOMS_CONTENTS_TYPE_MERCHANDISE='Merchandise' +STRING_SHIPPING_LABEL_CUSTOMS_CONTENTS_TYPE_DOCUMENTS='Documents' +STRING_SHIPPING_LABEL_CUSTOMS_CONTENTS_TYPE_GIFTS='Gifts' +STRING_SHIPPING_LABEL_CUSTOMS_CONTENTS_TYPE_SAMPLE='Sample' +STRING_SHIPPING_LABEL_CUSTOMS_CONTENTS_TYPE_OTHER='Other' +STRING_SHIPPING_LABEL_CUSTOMS_RESTRICTION_TYPE_NONE='None' +STRING_SHIPPING_LABEL_CUSTOMS_RESTRICTION_TYPE_QUARANTINE='Quarantine' +STRING_SHIPPING_LABEL_CUSTOMS_RESTRICTION_TYPE_SANITARY_INSPECTION='Sanitary / Phytosanitary inspection' +STRING_SHIPPING_LABEL_CUSTOMS_RESTRICTION_TYPE_OTHER='Other' +STRING_SHIPPING_LABEL_CUSTOMS_LINE_ITEM_RE='Custom\ Line\ .*' +STRING_SHIPPING_LABEL_CUSTOMS_LEARN_MORE_ITN_RE='.*\ about\ Internal\ Transaction\ Number' +STRING_SHIPPING_LABEL_CUSTOMS_LEARN_MORE_HS_TARIFF_NUMBER_RE='.*\ about\ HS\ Tariff\ number' +STRING_SHIPPING_LABEL_CUSTOMS_VALUE_HINT_RE='Value\ \(.*\ per\ unit\)' +STRING_SHIPPING_LABEL_CUSTOMS_WEIGHT_HINT_RE='Weight\ \(.*\ per\ unit\)' +STRING_SHIPPING_LABEL_CUSTOMS_CONTENTS_TYPE_DESCRIPTION_MISSING='Please describe what kind of goods this package contains.' +STRING_SHIPPING_LABEL_CUSTOMS_RESTRICTION_TYPE_DESCRIPTION_MISSING='Please describe what kind of restrictions this package must have.' +STRING_SHIPPING_LABEL_CUSTOMS_REQUIRED_FIELD='This field is required' +STRING_SHIPPING_LABEL_CUSTOMS_ITEM_DESCRIPTION_TOO_SHORT='You must provide a clear, specific description for every item.' +STRING_SHIPPING_LABEL_CUSTOMS_WEIGHT_ZERO_ERROR='Weight must be greater than zero' +STRING_SHIPPING_LABEL_CUSTOMS_VALUE_ZERO_ERROR='Declared value must be greater than zero' +STRING_SHIPPING_LABEL_CUSTOMS_FORM='Customs form' +STRING_SHIPPING_LABEL_PRINT_CUSTOMS_EXPLANATION='A customs form must be printed and included on this international shipment' +STRING_SHIPPING_LABEL_PRINT_CUSTOMS_FORM='Print customs form' +STRING_SHIPPING_LABEL_MULTIPLE_CUSTOMS_FORM_PRINT_BUTTON='Print' +STRING_SHIPPING_LABEL_PRINT_CUSTOMS_FORM_SCREEN_TITLE='Print customs invoice' +STRING_SHIPPING_LABEL_PRINT_CUSTOMS_FORM_DOWNLOAD_FAILED='Error downloading the customs form' +STRING_SHIPPING_LABEL_MOVE_ITEM_DIALOG_TITLE='Move item' +STRING_SHIPPING_LABEL_MOVE_ITEM_DIALOG_MOVE='Move' +STRING_SHIPPING_LABEL_MOVE_ITEM_DIALOG_CANCEL='Cancel' +STRING_SHIPPING_LABEL_MOVE_ITEM_DIALOG_DESCRIPTION_RE='This\ item\ is\ currently\ in\ .*\.\ Where\ would\ you\ like\ to\ move\ it\?' +STRING_SHIPPING_LABEL_MOVE_ITEM_DIALOG_NEW_PACKAGE_OPTION='Add to new package' +STRING_SHIPPING_LABEL_MOVE_ITEM_DIALOG_ORIGINAL_PACKAGING_OPTION='Ship in original packaging' +STRING_SHIPPING_LABEL_INSTALL_WC_SHIPPING_BANNER_TITLE='Need a shipping label?' +STRING_SHIPPING_LABEL_INSTALL_WC_SHIPPING_BANNER_DESCRIPTION='Print labels from your phone, with WooCommerce Shipping.' +STRING_SHIPPING_LABEL_INSTALL_WC_SHIPPING_BANNER_BUTTON='Get WooCommerce Shipping' +STRING_ORDER_REFUNDS_TITLE_WITH_AMOUNT_RE='Refund\ .*' +STRING_ORDER_REFUNDS_CONFIRMATION='Are you sure you want to issue a refund? This can’t be undone.' +STRING_ORDER_REFUNDS_NEXT_BUTTON_TITLE='Next' +STRING_ORDER_REFUNDS_REFUND='Refund' +STRING_ORDER_REFUNDS_REFUND_HIGH_ERROR='The refund is higher than the available amount' +STRING_ORDER_REFUNDS_REFUND_ZERO_ERROR='The refund must be greater than 0' +STRING_ORDER_REFUNDS_PREVIOUSLY_REFUNDED='Previously refunded' +STRING_ORDER_REFUNDS_REFUND_AMOUNT='Refund amount' +STRING_ORDER_REFUNDS_REASON='Reason for refund' +STRING_ORDER_REFUNDS_REASON_HINT='Reason for refund (optional)' +STRING_ORDER_REFUNDS_REFUND_VIA='Refund via' +STRING_ORDER_REFUNDS_REFUNDED_VIA_RE='Refunded\ via\ .*' +STRING_ORDER_REFUNDS_METHOD_RE='.*\ via\ .*' +STRING_ORDER_REFUNDS_REFUND_DETAILS='Refund details' +STRING_ORDER_REFUNDS_MANUAL_REFUND='Manual refund' +STRING_ORDER_REFUNDS_GIFT_CARD_UNSUPPORTED_TITLE='Refund from your store admin' +STRING_ORDER_REFUNDS_GIFT_CARD_UNSUPPORTED_MESSAGE='This order was paid with a gift card. Refunding here won'"'"'t restore the gift-card balance. To refund correctly, please process it from your store admin on the web.' +STRING_ORDER_REFUNDS_CREDIT_CARD_REFUND='Credit card' +STRING_ORDER_REFUNDS_QUOTE_IMAGE_DESCRIPTION='Quotation icon' +STRING_ORDER_REFUNDS_AMOUNT_REFUND_PROGRESS_MESSAGE_RE='Your\ refund\ for\ .*\ is\ being\ processed\.\ Please\ wait…' +STRING_ORDER_REFUNDS_AMOUNT_REFUND_CONFIRMATION_MESSAGE='Waiting for refund confirmation…' +STRING_ORDER_REFUNDS_AMOUNT_REFUND_SUCCESSFUL='The refund was successfully submitted.' +STRING_ORDER_REFUNDS_AMOUNT_REFUND_ERROR='Something went wrong with the refund. Please try again.' +STRING_ORDER_REFUNDS_REFUND_MANUAL_REFUND_NOTE='The payment method does not support automatic refunds. Complete the refund by transferring the money to the customer manually.' +STRING_ORDER_REFUNDS_ITEMS_SELECT_ALL='Select all' +STRING_ORDER_REFUNDS_ITEMS_SELECT_NONE='Select none' +STRING_ORDER_REFUNDS_ITEMS_SELECTED_RE='.*\ items\ selected' +STRING_ORDER_REFUNDS_ITEM_DESCRIPTION_RE='.*\ x\ .*\ each' +STRING_ORDER_REFUNDS_DETAIL_ITEM_DESCRIPTION_RE='.*\ \(.*\ x\ .*\)' +STRING_ORDER_REFUNDS_PRODUCTS_REFUND='Products refund' +STRING_ORDER_REFUNDS_SHIPPING_REFUND='Shipping refund' +STRING_ORDER_REFUNDS_CUSTOM_AMOUNT_REFUND='Custom amount refund' +STRING_ORDER_REFUNDS_SELECT_QUANTITY='Select quantity' +STRING_ORDER_REFUNDS_REFUND_SHIPPING='Refund shipping' +STRING_ORDER_REFUNDS_REFUND_CUSTOM_AMOUNT='Refund Custom Amount' +STRING_ORDER_REFUNDS_REFUND_IN_PROGRESS='Refund in progress, please wait…' +STRING_ORDER_REFUNDS_REFUND_INFO_DESCRIPTION_ONE_RE='.*\ item' +STRING_ORDER_REFUNDS_REFUND_INFO_DESCRIPTION_MANY_RE='.*\ items' +STRING_ORDER_REFUNDS_REFUND_INFO_TITLE='Refunded products' +STRING_ORDER_REFUNDS_SHIPPING_REFUND_VARIABLE_NOTICE_RE='You\ can\ refund\ .*' +STRING_ORDER_REFUNDS_STORE_ADMIN_LINK_TEXT='in your store admin' +STRING_ADD_ORDER_NOTE_LABEL='Email note to customer' +STRING_ADD_ORDER_NOTE_SUBLABEL='If disabled the note will be private' +STRING_ADD_ORDER_NOTE_MENU_ITEM='Add' +STRING_ADD_ORDER_NOTE_ADDED='Order note added' +STRING_ADD_ORDER_NOTE_ERROR='Unable to add note' +STRING_ADD_ORDER_NOTE_PROGRESS_TITLE='Adding note' +STRING_ADD_ORDER_NOTE_PROGRESS_MESSAGE='Please wait' +STRING_ORDERSTATUS_SELECT_STATUS='Change order status' +STRING_ORDERSTATUS_CONTENTDESC_WITHSTATUS_RE='Order\ status:\ .*' +STRING_ORDER_SHIPMENT_TRACKING_TOOLBAR_TITLE='Add Tracking' +STRING_ORDER_SHIPMENT_TRACKING_CARRIER_LABEL='Carrier' +STRING_ORDER_SHIPMENT_TRACKING_NUMBER_LABEL='Tracking number' +STRING_ORDER_SHIPMENT_TRACKING_CUSTOM_PROVIDER_NAME_LABEL='Carrier name' +STRING_ORDER_SHIPMENT_TRACKING_CUSTOM_PROVIDER_URL_LABEL='Tracking link (optional)' +STRING_ORDER_SHIPMENT_TRACKING_DATE_LABEL='Date shipped' +STRING_ORDER_SHIPMENT_TRACKING_PROVIDER_TOOLBAR_TITLE='Shipping Carriers' +STRING_ORDER_SHIPMENT_TRACKING_PROVIDER_LIST_ITEM='Selected Shipment carrier' +STRING_ORDER_SHIPMENT_TRACKING_PROVIDER_LIST_ERROR_FETCH_GENERIC='Error fetching carriers' +STRING_ORDER_SHIPMENT_TRACKING_PROVIDER_LIST_ERROR_EMPTY_LIST='No carriers found' +STRING_ORDER_SHIPMENT_TRACKING_ADDED='Shipment tracking added' +STRING_ORDER_SHIPMENT_TRACKING_ERROR='Unable to add tracking' +STRING_ORDER_SHIPMENT_TRACKING_EMPTY_PROVIDER='Please select a carrier' +STRING_ORDER_SHIPMENT_TRACKING_EMPTY_TRACKING_NUM='Please enter a tracking number' +STRING_ORDER_SHIPMENT_TRACKING_EMPTY_CUSTOM_PROVIDER_NAME='Please enter a carrier name' +STRING_ORDER_SHIPMENT_TRACKING_CUSTOM_PROVIDER_SECTION_TITLE='Custom' +STRING_ORDER_SHIPMENT_TRACKING_CUSTOM_PROVIDER_SECTION_NAME='Custom Carrier' +STRING_ORDER_SHIPMENT_TRACKING_PROGRESS_TITLE='Adding tracking' +STRING_ORDER_SHIPMENT_TRACKING_PROGRESS_MESSAGE='Please wait' +STRING_CARD_READER_UPSELL_CARD_READER_BANNER_DISMISS='Dismiss' +STRING_CARD_READER_UPSELL_CARD_READER_BANNER_HIDE_CONTENT='Hide this content' +STRING_CARD_READER_UPSELL_CARD_READER_BANNER_NEW='NEW' +STRING_CARD_READER_UPSELL_CARD_READER_BANNER_TITLE='Accept payments easily' +STRING_CARD_READER_UPSELL_CARD_READER_BANNER_DESCRIPTION='Start selling in person in under 20 minutes with our card reader.' +STRING_CARD_READER_UPSELL_CARD_READER_BANNER_CTA='Purchase Card Reader' +STRING_PAYMENTS_HUB_TITLE='Payments' +STRING_CARD_READER_MANAGE_CARD_READER='Manage Card Reader' +STRING_CARD_READER_SETTINGS_HEADER='SETTINGS' +STRING_CARD_READER_ENABLE_PAY_IN_PERSON='Pay In Person' +STRING_CARD_READER_ENABLE_PAY_IN_PERSON_DESCRIPTION='The Pay In Person checkout option lets you accept payments for website orders, on collection or delivery. Learn more' +STRING_CARD_READER_PURCHASE_CARD_READER='Order Card Reader' +STRING_CARD_READER_MANAGE_PAYMENT_PROVIDER='Change Payment Provider' +STRING_CARD_READER_ICON_CONTENT_DESCRIPTION='Card Reader Image' +STRING_CARD_READER_ONBOARDING_NOT_FINISHED='We’ve noticed that you have not yet finished In-Person Payments setup. Continue setup' +STRING_CARD_READER_ONBOARDING_WITH_PENDING_REQUIREMENTS='There is an issue that requires your attention. Please take a look' +STRING_CARD_READER_TEST_TAP_TO_PAY='Try out Tap To Pay' +STRING_CARD_READER_TAP_TO_PAY_HEADER='TAP TO PAY' +STRING_CARD_READER_ABOUT_TAP_TO_PAY='About Tap to Pay' +STRING_CARD_READER_TAP_TO_PAY_DESCRIPTION='Use your phone to accept card\npayments. Try it now.' +STRING_CARD_READER_TAP_TO_PAY_NOT_AVAILABLE_ERROR_CHECK_REQUIREMENTS_BUTTON='Check Requirements' +STRING_CARD_READER_TAP_TO_PAY_NOT_AVAILABLE_ERROR_TITLE='Tap to Pay is not available' +STRING_CARD_READER_TAP_TO_PAY_NOT_AVAILABLE_ERROR_NFC='To use Tap To Pay on Android, your device needs an NFC chip. To accept in-person payments, please purchase a Bluetooth card reader.' +STRING_CARD_READER_TAP_TO_PAY_NOT_AVAILABLE_ERROR_ANDROID_VERSION='To use Tap To Pay on Android, you need Android 10 or newer. To accept in-person payments, please update Android or purchase a Bluetooth card reader.' +STRING_CARD_READER_TAP_TO_PAY_NOT_AVAILABLE_ERROR_GMS='To use Tap To Pay on Android, your device needs Google Play Services. To accept in-person payments, please install Google Play Services or purchase a Bluetooth card reader.' +STRING_CARD_READER_TAP_TO_PAY_NOT_AVAILABLE_ERROR_COUNTRY='Unfortunately, Tap To Pay on Android is not available in your country yet. Stay tuned!' +STRING_CARD_READER_TAP_TO_PAY_NOT_AVAILABLE_ERROR_DEVICE='This device doesn'"'"'t meet the security requirements for Tap to Pay on Android. To accept in-person payments, you can purchase a Bluetooth card reader.' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_AVAILABLE_FUNDS='Available funds' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_PENDING_FUNDS='Pending funds' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_FUNDS_AVAILABLE_AFTER_PLURAL_RE='Funds\ become\ available\ after\ pending\ for\ .*\ days\.' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_FUNDS_AVAILABLE_AFTER_ONE_RE='Funds\ become\ available\ after\ pending\ for\ .*\ day\.' +STRING_CARD_READER_HUB_PAYOUT_FUNDS_PAYOUT_TITLE='LAST PAYOUT' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_AVAILABLE_PAYOUT_TIME_DAILY='Available funds are paid out automatically, every day.' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_AVAILABLE_PAYOUT_TIME_WEEKLY_RE='Available\ funds\ are\ paid\ out\ automatically,\ every\ .*\.' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_AVAILABLE_PAYOUT_TIME_MONTHLY_RE='Available\ funds\ are\ paid\ out\ automatically,\ every\ month\ on\ the\ .*\.' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_LEARN_MORE='Learn more about when you'"'"'ll receive your funds' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_COLLAPSE_EXPAND_CONTENT_DESCRIPTION='Collapse/expand payout summary' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_STATUS_ESTIMATED='Estimated' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_STATUS_PAID='Paid' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_STATUS_PENDING='Pending' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_STATUS_IN_TRANSIT='In transit' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_STATUS_CANCELED='Canceled' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_STATUS_FAILED='Failed' +STRING_CARD_READER_HUB_PAYOUT_SUMMARY_STATUS_UNKNOWN='Unknown' +STRING_CARD_READER_TAP_TO_PAY_EXPLANATION_SCREEN_TITLE='Tap To Pay' +STRING_CARD_READER_TAP_TO_PAY_EXPLANATION_TITLE='Collect card payments\nwith your phone' +STRING_CARD_READER_TAP_TO_PAY_EXPLANATION_READY='Accept all types of in-person payments, right\non your phone. No extra hardware needed.' +STRING_CARD_READER_TAP_TO_PAY_EXPLANATION_EASY='It’s easy, secure, and private.' +STRING_CARD_READER_TAP_TO_PAY_EXPLANATION_TRY_AND_REFUND_WITH_AMOUNT_RE='Try\ a\ .*\ payment\ with\ your\ debit\ or\ credit\ card\.\\nThe\ payment\ will\ be\ refunded\ when\ you’re\ done\.' +STRING_CARD_READER_TAP_TO_PAY_EXPLANATION_WHERE_TO_FIND='Choose the Tap to Pay from the Collect Payment options in\nOrder Details or Menu > Payments.' +STRING_CARD_READER_TAP_TO_PAY_EXPLANATION_TRY_PAYMENT='Try a payment' +STRING_CARD_READER_TAP_TO_PAY_EXPLANATION_TEST_PAYMENT_ERROR='Something went wrong. Please try again later.' +STRING_CARD_READER_TAP_TO_PAY_EXPLANATION_REFUNDING_PAYMENT='Refunding test payment…' +STRING_CARD_READER_TAP_TO_PAY_EXPLANATION_REFUND_FAILED='The refund failed. Try to refund manually' +STRING_CARD_READER_TAP_TO_PAY_TEST_PAYMENT_NOTE='Tap To Pay Test Payment' +STRING_CARD_READER_TAP_TO_PAY_SUCCESSFUL_REFUND_MESSAGE='Test Tap To Pay payment was successfully refunded' +STRING_CARD_READER_TAP_TO_PAY_SUCCESSFUL_REFUND_ACTION_LABEL='View Order' +STRING_CARD_READER_TAP_TO_PAY_LEARN_MORE='Learn more about accepting payments with Tap To Pay on Android' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_TITLE='About Tap To Pay' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_HEADER='What is Tap to Pay?' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_DESCRIPTION='Tap to Pay lets you accept all types of contactless payments – from physical debit and credit cards, to digital wallets – without the need to purchase a physical card reader.' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_IMPORTANT_INFO_TITLE='Important information' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_IMPORTANT_INFO_DESCRIPTION_1_RE='In\ .*,\ some\ cards\ require\ a\ PIN\ for\ contactless\ transactions\ above\ .*\.' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_IMPORTANT_INFO_DESCRIPTION_2='We do not support PIN entry with Tap to Pay on Android.' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_IMPORTANT_INFO_DESCRIPTION_3='To accept payments above this limit, consider purchasing a card reader that accepts PIN entry.' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_IMPORTANT_INFO_BUTTON='Learn more about card readers' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_HOW_WORKS_TITLE='How it works' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_HOW_WORKS_1_UPDATED='1. Create an order' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_HOW_WORKS_2='2. Tap “Collect Payment” and choose “Tap to Pay”.' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_HOW_WORKS_3='3. Present your phone to the customer.' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_HOW_WORKS_4='4. Your customer taps their card on the back of your phone.' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_HOW_WORKS_5='5. After you see the “Done” checkmark, your store will process the payment, and the transaction will be complete.' +STRING_CARD_READER_TAP_TO_PAY_ABOUT_COPYRIGHT='The Contactless Symbol is a trademark owned by and used with permission of EMVCo, LLC.' +STRING_CARD_READER_TYPE_SELECTION_TAP_TO_PAY='Tap To Pay' +STRING_CARD_READER_TYPE_SELECTION_TAP_TO_PAY_DESCRIPTION='Securely accept contactless payments directly from your phone.' +STRING_CARD_READER_TYPE_SELECTION_BLUETOOTH_READER='Card Reader' +STRING_CARD_READER_TYPE_SELECTION_BLUETOOTH_READER_DESCRIPTION='Card reader accepts tap, chip, and swipe payments with debit and credit cards.' +STRING_TAP_TO_PAY_REFUND_REASON='Test Tap To Pay payment auto refund' +STRING_CARD_READER_TYPE_SELECTION_SCAN_TO_PAY='Scan To Pay' +STRING_SCAN_TO_PAY_TITLE='Scan QR and follow instructions' +STRING_CARD_READER_INTERAC_REFUND_REFUND_LOADING_HEADER='Getting ready to refund payment' +STRING_CARD_READER_INTERAC_REFUND_REFUND_FAILED_HEADER='Refund failed' +STRING_CARD_READER_INTERAC_REFUND_REFUND_PAYMENT='Refund payment' +STRING_CARD_READER_INTERAC_REFUND_REFUND_PROCESSING_STATE='Processing refund' +STRING_CARD_READER_INTERAC_REFUND_REFUND_COMPLETED_HEADER='Refund successful' +STRING_CARD_READER_INTERAC_REFUND_REFUND_FAILED_UNEXPECTED_ERROR_STATE='Sorry, this refund couldn\’t be processed' +STRING_CARD_READER_INTERAC_REFUND_REFUND_FAILED_FRAUD='Try another means of refund' +STRING_CARD_READER_INTERAC_REFUND_REFUND_FAILED_GENERIC='Refund was declined for an unspecified reason' +STRING_CARD_READER_INTERAC_REFUND_REFUND_FAILED_CARD_NOT_SUPPORTED='The card does not support this type of refund' +STRING_CARD_READER_INTERAC_REFUND_REFUND_FAILED_DUPLICATE_TRANSACTION='An identical refund was submitted recently' +STRING_CARD_READER_INTERAC_REFUND_REFUND_FAILED_INSUFFICIENT_FUNDS='Refund declined due to insufficient funds' +STRING_CARD_READER_INTERAC_REFUND_REFUND_FAILED_INVALID_AMOUNT='The refund amount is not allowed for the card presented' +STRING_CARD_READER_INTERAC_REFUND_REFUND_FAILED_TEST_CARD='System test cards are not permitted for refund' +STRING_CARD_READER_INTERAC_REFUND_REFUND_FAILED_OK='OK' +STRING_CARD_READER_INTERAC_REFUND_REFUND_FAILED_CANCELLED='Refund cancelled' +STRING_CARD_READER_INTERAC_REFUND_ORDER_REFUNDED_REFUND_CANCELLED='The order is already refunded' +STRING_CARD_READER_INTERAC_REFUND_REFUND_PAYMENT_HINT='Tap or insert to refund' +STRING_CARD_READER_INTERAC_REFUND_NOTIFYING_BACKEND_ABOUT_SUCCESSFUL_REFUND='Applying refund to order' +STRING_CARD_READER_INTERAC_REFUND_NOTIFYING_BACKEND_ABOUT_SUCCESSFUL_REFUND_FAILED='Something went wrong while applying the refund' +STRING_CARD_READER_TAP_OR_INSERT='Tap or insert to pay' +STRING_CARD_READER_PAYMENT_COLLECT_PAYMENT_LOADING_HEADER='Getting ready to collect payment' +STRING_CARD_READER_PAYMENT_COLLECT_PAYMENT_LOADING_PAYMENT_STATE='Connecting to reader' +STRING_CARD_READER_PAYMENT_FETCH_ORDER_LOADING_HEADER='Updating the app state' +STRING_CARD_READER_PAYMENT_FETCH_ORDER_LOADING_PAYMENT_STATE='Refreshing order' +STRING_CARD_READER_COLLECT_PAYMENT='Collect payment' +STRING_CARD_READER_CARD_READERS_HEADER='CARD READERS' +STRING_CARD_READER_PAYMENT_COMPLETED_PAYMENT_HEADER='Payment successful' +STRING_CARD_READER_PAYMENT_PAYMENT_FAILED_HEADER='Payment failed' +STRING_CARD_READER_PAYMENT_PAYMENT_FAILED_OK='OK' +STRING_CARD_READER_PAYMENT_PAYMENT_FAILED_PURCHASE_HARDWARE_READER='Buy a card reader' +STRING_CARD_READER_PAYMENT_COLLECT_PAYMENT_STATE='Reader is ready' +STRING_CARD_READER_PAYMENT_COLLECT_PAYMENT_BUILT_IN_STATE='Built-in reader is ready' +STRING_CARD_READER_PAYMENT_PROCESSING_PAYMENT_STATE='Processing payment' +STRING_CARD_READER_PAYMENT_CAPTURING_PAYMENT_STATE='Capturing payment' +STRING_CARD_READER_PAYMENT_FAILED_NO_NETWORK_STATE='No internet connection' +STRING_CARD_READER_PAYMENT_FAILED_SERVER_ERROR_STATE='No connection to server' +STRING_CARD_READER_PAYMENT_FAILED_UNEXPECTED_ERROR_STATE='Sorry, this payment couldn\’t be processed' +STRING_CARD_READER_PAYMENT_FAILED_AMOUNT_TOO_SMALL_RE='Amount\ must\ be\ at\ least\ .*' +STRING_CARD_READER_PAYMENT_FAILED_TEMPORARY='Trying again may succeed' +STRING_CARD_READER_PAYMENT_FAILED_FRAUD='Try another means of payment' +STRING_CARD_READER_PAYMENT_FAILED_GENERIC='Payment was declined for an unspecified reason' +STRING_CARD_READER_PAYMENT_FAILED_INVALID_ACCOUNT='The card or card account is invalid' +STRING_CARD_READER_PAYMENT_FAILED_CARD_NOT_SUPPORTED='The card does not support this type of purchase' +STRING_CARD_READER_PAYMENT_FAILED_CURRENCY_NOT_SUPPORTED='The card does not support this currency' +STRING_CARD_READER_PAYMENT_FAILED_DUPLICATE_TRANSACTION='An identical transaction was submitted recently' +STRING_CARD_READER_PAYMENT_FAILED_EXPIRED_CARD='The card has expired' +STRING_CARD_READER_PAYMENT_FAILED_INCORRECT_POSTAL_CODE='The transaction postal code and card postal code do not match' +STRING_CARD_READER_PAYMENT_FAILED_INSUFFICIENT_FUNDS='Payment declined due to insufficient funds' +STRING_CARD_READER_PAYMENT_FAILED_INVALID_AMOUNT='The payment amount is not allowed for the card presented' +STRING_CARD_READER_PAYMENT_FAILED_PIN_REQUIRED='This card requires a PIN code and thus cannot be processed' +STRING_CARD_READER_PAYMENT_FAILED_INCORRECT_PIN='An incorrect PIN has been entered. Try again, or use another means of payment' +STRING_CARD_READER_PAYMENT_FAILED_PIN_REQUIRED_TAP_TO_PAY='A PIN code is required, but Tap To Pay doesn'"'"'t support it yet. Consider using an external card reader' +STRING_CARD_READER_PAYMENT_FAILED_TOO_MANY_PIN_TRIES='An incorrect PIN has been entered too many times' +STRING_CARD_READER_PAYMENT_FAILED_TEST_CARD='System test cards are not permitted for payment' +STRING_CARD_READER_PAYMENT_FAILED_TEST_MODE_LIVE_CARD='A live card was used on a site in test mode' +STRING_CARD_READER_PAYMENT_FAILED_UNKNOWN='Payment was declined for an unknown reason' +STRING_CARD_READER_PAYMENT_FAILED_CANCELED='Transaction was canceled' +STRING_CARD_READER_MODE_READY_TO_PAIR_HEADER='Ready to pair' +STRING_CARD_READER_MODE_READY_TO_PAIR_SUBTITLE='Open Woo POS on your tablet and pick this phone.' +STRING_CARD_READER_MODE_READY_TO_PAIR_STORE_RE='Store:\ .*' +STRING_CARD_READER_MODE_WAITING_HEADER='Paired with tablet' +STRING_CARD_READER_MODE_WAITING_SUBTITLE='Waiting for payment…' +STRING_CARD_READER_MODE_CANCEL='Cancel' +STRING_CARD_READER_MODE_SETTINGS_ROW_LABEL='Card Reader Mode' +STRING_CARD_READER_MODE_STARTING_HEADER='Starting Card Reader Mode' +STRING_CARD_READER_MODE_STARTING_SUBTITLE='Setting up discovery on the local network…' +STRING_CARD_READER_MODE_ERROR_HEADER='Card Reader Mode failed to start' +STRING_CARD_READER_MODE_ERROR_SUBTITLE='Check your Wi-Fi connection and try again.' +STRING_CARD_READER_MODE_ERROR_CLOSE='Close' +STRING_CARD_READER_MODE_LOCATION_PERMISSION_HEADER='Location permission required' +STRING_CARD_READER_MODE_LOCATION_PERMISSION_SUBTITLE='Android requires location access to use tap-to-pay on this phone. Your location stays on this device.' +STRING_CARD_READER_MODE_LOCATION_PERMISSION_CONTINUE='Continue' +STRING_CARD_READER_MODE_LOCATION_PERMISSION_DENIED_SUBTITLE='Location permission is blocked. Open Settings and grant Location to use tap-to-pay on this phone.' +STRING_CARD_READER_MODE_LOCATION_PERMISSION_OPEN_SETTINGS='Open Settings' +STRING_CARD_READER_PAYMENT_FAILED_NFC_DISABLED='The app could not enable the card reader, because the NFC chip is disabled' +STRING_CARD_READER_PAYMENT_FAILED_NFC_DISABLED_ENABLE_NFC='Enable NFC' +STRING_CARD_READER_PAYMENT_FAILED_DEVICE_IS_NOT_SUPPORTED='Your device is not supported. Please contact support for more details' +STRING_CARD_READER_PAYMENT_FAILED_APP_SETUP_IS_INVALID='Something went wrong with the app setup. Please contact support for more details' +STRING_CARD_READER_PAYMENT_RETRY_CARD_PROMPT='Retry with the same card' +STRING_CARD_READER_PAYMENT_REMOVE_CARD_PROMPT='Remove the card' +STRING_CARD_READER_PAYMENT_MULTIPLE_CONTACTLESS_CARDS_DETECTED_PROMPT='Multiple cards detected. Try again with a single card' +STRING_CARD_READER_PAYMENT_TRY_ANOTHER_READ_METHOD_PROMPT='Retry tapping, inserting or swiping your card' +STRING_CARD_READER_PAYMENT_TRY_ANOTHER_CARD_PROMPT='Retry with another card' +STRING_CARD_READER_PAYMENT_CHECK_MOBILE_DEVICE_PROMPT='Check your mobile device' +STRING_CARD_READER_PAYMENT_CARD_REMOVED_TOO_EARLY='The card was removed too early' +STRING_CARD_READER_PAYMENT_PRINT_RECEIPT='Print receipt' +STRING_CARD_READER_PAYMENT_SEND_RECEIPT='Send receipt' +STRING_CARD_READER_PAYMENT_SAVE_FOR_LATER='Save receipt and continue' +STRING_CARD_READER_PAYMENT_UPDATE_AVAILABLE='An update for the card reader is available' +STRING_CARD_READER_PAYMENT_DESCRIPTION_V2_RE='In\-Person\ Payment\ for\ Order\ \#.*\ for\ .*\ blog_id\ .*\.' +STRING_CARD_READER_PAYMENT_RECEIPT_EMAIL_SUBJECT_RE='Your\ receipt\ from\ .*' +STRING_CARD_READER_PAYMENT_EMAIL_CLIENT_NOT_FOUND='Unable to detect any application to which the receipt can be shared' +STRING_CARD_READER_PAYMENT_RECEIPT_CAN_NOT_BE_DOWNLOADED='Unable to download the receipt' +STRING_CARD_READER_PAYMENT_RECEIPT_CAN_NOT_BE_STORED='Unable to store the receipt' +STRING_CARD_READER_REFETCHING_ORDER_FAILED='Error fetching order. Order state in the app might be outdated.' +STRING_CARD_READER_PAYMENT_ORDER_PAID_PAYMENT_CANCELLED='The order is already paid' +STRING_CARD_READER_PAYMENT_READER_NOT_CONNECTED='Please make sure that the card reader is connected.' +STRING_CARD_READER_PAYMENT_READER_RECEIPT_SENT_RE='A\ receipt\ has\ been\ sent\ to\ .*' +STRING_CARD_READER_PAYMENT_VM_KILLED_WHEN_TPP_IN_FOREGROUND='The system terminated the Woo app while it was running in the background. You may attempt to use it again.' +STRING_CARD_READER_WELCOME_DIALOG_HEADER='Collect payments with a card reader' +STRING_CARD_READER_WELCOME_DIALOG_TEXT='Congrats, you are now able to accept debit and credit card payments with WooCommerce Payments!' +STRING_CARD_READER_CONNECT_SCANNING_HEADER='Scanning for readers' +STRING_CARD_READER_CONNECT_SCANNING_BUILT_IN_HEADER='Preparing built-in reader…' +STRING_CARD_READER_CONNECT_SCANNING_HINT='Press the power button of your reader' +STRING_CARD_READER_CONNECT_SCANNING_BUILT_IN_HINT='It won'"'"'t take long' +STRING_CARD_READER_CONNECT_READER_FOUND_HEADER_RE='Do\ you\ want\ to\ connect\ reader\ .*\?' +STRING_CARD_READER_CONNECT_MULTIPLE_READERS_FOUND_HEADER='Several readers found' +STRING_CARD_READER_CONNECT_TO_READER='Connect to reader' +STRING_CARD_READER_CONNECT_CONNECT_BUTTON='Connect' +STRING_CARD_READER_CONNECT_KEEP_SEARCHING_BUTTON='Keep Searching' +STRING_CARD_READER_CONNECT_CONNECTING_HEADER='Connecting to reader' +STRING_CARD_READER_CONNECT_CONNECTING_BUILT_IN_HEADER='Preparing for payment' +STRING_CARD_READER_CONNECT_FAILED_HEADER='We couldn\’t connect your reader' +STRING_CARD_READER_CONNECT_MISSING_ADDRESS='Please provide your store address in order to proceed' +STRING_CARD_READER_CONNECT_MISSING_ADDRESS_BUTTON='Enter address' +STRING_CARD_READER_CONNECT_INVALID_POSTAL_CODE_HEADER='Store address postcode is invalid' +STRING_CARD_READER_CONNECT_INVALID_POSTAL_CODE_HINT='Please provide a valid postcode in your store settings and try again' +STRING_CARD_READER_CONNECT_SCANNING_FAILED_HEADER='No reader connected' +STRING_CARD_READER_CONNECT_MISSING_PERMISSIONS_HEADER='Missing required precise location permission' +STRING_CARD_READER_CONNECT_LOCATION_PROVIDER_DISABLED_HEADER='Location is disabled' +STRING_CARD_READER_CONNECT_BLUETOOTH_DISABLED_HEADER='Bluetooth is disabled' +STRING_CARD_READER_CONNECT_PERMISSION_RATIONALE_HEADER='Location Access Required' +STRING_CARD_READER_CONNECT_PERMISSION_RATIONALE_HINT='Card reader payments require precise location permission' +STRING_CARD_READER_CONNECT_OPEN_PERMISSION_SETTINGS='Open settings' +STRING_CARD_READER_CONNECT_OPEN_LOCATION_SETTINGS='Open settings' +STRING_CARD_READER_CONNECT_OPEN_BLUETOOTH_SETTINGS='Turn on bluetooth' +STRING_CARD_READER_CONNECT_MISSING_BLUETOOTH_PERMISSIONS_HEADER='Missing required nearby devices permission' +STRING_CARD_READER_CONNECT_MISSING_BLUETOOTH_PERMISSION_BUTTON='Open settings' +STRING_CARD_READER_CONNECT_LEARN_MORE='Learn more about In-Person Payments' +STRING_CARD_READER_CONNECT_FAILED_BATTERY_LOW_HINT='The reader battery is low. Please charge the reader and try again' +STRING_CARD_READER_CONNECT_FAILED_BLUETOOTH_PAIRING_REMOVED_HINT='Forget the reader in Android Bluetooth Settings' +STRING_CARD_READER_DETAIL_NOT_CONNECTED_HEADER='Connect your card reader' +STRING_CARD_READER_DETAIL_RECONNECTING_HEADER='Reconnecting to card reader…' +STRING_CARD_READER_DETAIL_RECONNECTING_CANCEL='Cancel reconnection' +STRING_CARD_READER_DETAIL_LEARN_MORE='Learn more about accepting mobile payments and ordering card readers' +STRING_CARD_READER_DETAIL_NOT_CONNECTED_FIRST_HINT_LABEL='Make sure card reader is charged' +STRING_CARD_READER_DETAIL_NOT_CONNECTED_SECOND_HINT_LABEL='Turn card reader on and place it next to mobile device' +STRING_CARD_READER_DETAIL_NOT_CONNECTED_THIRD_HINT_LABEL='Turn mobile device bluetooth on' +STRING_CARD_READER_DETAILS_NOT_CONNECTED_CONNECT_BUTTON_LABEL='Connect card reader' +STRING_CARD_READER_DETAIL_CONNECTED_HEADER='CONNECTED READER' +STRING_CARD_READER_DETAIL_CONNECTED_BATTERY_PERCENTAGE_RE='.*%%\ battery' +STRING_CARD_READER_DETAIL_CONNECTED_FIRMWARE_VERSION_RE='Firmware:\ .*' +STRING_CARD_READER_DETAIL_CONNECTED_UPDATE_SOFTWARE='Update reader'"'"'s software' +STRING_CARD_READER_DETAIL_CONNECTED_ENFORCED_UPDATE_SOFTWARE='Please update your reader software to keep accepting payments' +STRING_CARD_READER_DETAIL_CONNECTED_DISCONNECT_READER='Disconnect reader' +STRING_CARD_READER_DETAIL_CONNECTED_READER_UNKNOWN='UNKNOWN CARD READER'"'"'s NAME' +STRING_CARD_READER_DETAIL_CONNECTED_UPDATE_SUCCESS='Reader\’s software updated' +STRING_CARD_READER_DETAIL_CONNECTED_UPDATE_FAILED='Reader'"'"'s software update has failed' +STRING_CARD_READER_DETAIL_CONNECTED_READERS_NAME_CLIPBOARD='Reader'"'"'s serial number copied to the clipboard' +STRING_CARD_READER_SOFTWARE_UPDATE_DESCRIPTION='Your card reader’s software needs to be updated to keep running smoothly' +STRING_CARD_READER_SOFTWARE_UPDATE_TITLE='Software update' +STRING_CARD_READER_SOFTWARE_UPDATE_TITLE_BATTERY_LOW='Please charge reader' +STRING_CARD_READER_SOFTWARE_UPDATE_IN_PROGRESS_TITLE='Updating your reader'"'"'s software' +STRING_CARD_READER_SOFTWARE_UPDATE_PROGRESS_DESCRIPTION_LOW_BATTERY_RE='Updating\ the\ reader\ software\ failed\ because\ the\ reader\\’s\ battery\ is\ .*%%\ charged\.\ Please\ charge\ the\ reader\ above\ 50%%\ before\ trying\ again\.' +STRING_CARD_READER_SOFTWARE_UPDATE_PROGRESS_DESCRIPTION_LOW_BATTERY_LEVEL_UNKNOWN='Updating the reader software failed because the reader\’s battery is insufficiently charged. Please charge the reader above 50%% before trying again.' +STRING_CARD_READER_SOFTWARE_UPDATE_PROGRESS_CANCEL_WARNING='Canceling an ongoing software update is not recommended' +STRING_CARD_READER_SOFTWARE_UPDATE_PROGRESS_CANCEL_REQUIRED_WARNING='Canceling an ongoing software update is not recommended. Cancelling will block your reader connection.' +STRING_CARD_READER_SOFTWARE_UPDATE_PROGRESS_INDICATOR_RE='.*%%\ complete' +STRING_CARD_READER_ONBOARDING_TITLE='In-Person Payments' +STRING_CARD_READER_ONBOARDING_LOADING='Connecting to your account' +STRING_CARD_READER_ONBOARDING_COUNTRY_NOT_SUPPORTED_HEADER_RE='We\ don'"'"'t\ support\ Card\ In\-Person\ Payments\ in\ .*' +STRING_CARD_READER_ONBOARDING_COUNTRY_NOT_SUPPORTED_HINT='You can still accept In-Person Cash Payments by enabling the "cash on delivery" payment method on your store' +STRING_CARD_READER_ONBOARDING_COUNTRY_NOT_SUPPORTED_CONTACT_SUPPORT='Need some help? Contact support' +STRING_CARD_READER_ONBOARDING_COUNTRY_NOT_SUPPORTED_LEARN_MORE='Learn more about accepting payments with your mobile device and ordering card readers' +STRING_CARD_READER_ONBOARDING_STRIPE_UNSUPPORTED_IN_COUNTRY_HEADER_RE='We\ don'"'"'t\ support\ WooCommerce\ Stripe\ extension\ in\ .*' +STRING_CARD_READER_ONBOARDING_WCPAY_UNSUPPORTED_IN_COUNTRY_HEADER_RE='We\ don'"'"'t\ support\ WooCommerce\ Payments\ extension\ in\ .*' +STRING_CARD_READER_ONBOARDING_STRIPE_ACCOUNT_IN_UNSUPPORTED_COUNTRY_RE='We\ don'"'"'t\ support\ Stripe\ accounts\ registered\ in\ .*' +STRING_CARD_READER_ONBOARDING_WCPAY_NOT_INSTALLED_HEADER='Install WooCommerce Payments' +STRING_CARD_READER_ONBOARDING_WCPAY_NOT_INSTALLED_HINT='You\’ll need to install the free WooCommerce Payments extension on your store to accept In-Person Payments.' +STRING_CARD_READER_ONBOARDING_WCPAY_NOT_ACTIVATED_HEADER='Activate WooCommerce Payments' +STRING_CARD_READER_ONBOARDING_WCPAY_NOT_ACTIVATED_HINT='The WooCommerce Payments extension is installed on your store but not activated. Please activate it to accept In-Person Payments.' +STRING_CARD_READER_ONBOARDING_WCPAY_NOT_ACTIVATED_ACTIVATE_BUTTON='Activate' +STRING_CARD_READER_ONBOARDING_WCPAY_NOT_SETUP_HEADER='Finish setup WooCommerce Payments in your store admin' +STRING_CARD_READER_ONBOARDING_WCPAY_NOT_SETUP_HINT='You’re almost there! Please finish setting up WooCommerce Payments to start accepting In-Person Payments.' +STRING_CARD_READER_ONBOARDING_WCPAY_NOT_SETUP_GO_TO_WPADMIN_BUTTON='Finish setup' +STRING_CARD_READER_ONBOARDING_WCPAY_UNSUPPORTED_VERSION_HEADER='Update WooCommerce Payments' +STRING_CARD_READER_ONBOARDING_WCPAY_UNSUPPORTED_VERSION_HINT='Outdated version of the WooCommerce Payments extension is installed on your store. Please update it to accept In-Person Payments.' +STRING_CARD_READER_ONBOARDING_WCPAY_UNSUPPORTED_VERSION_REFRESH_BUTTON='Refresh after updating' +STRING_CARD_READER_ONBOARDING_STRIPE_EXTENSION_NOT_SETUP_HEADER='Finish Stripe setup on your store admin' +STRING_CARD_READER_ONBOARDING_STRIPE_EXTENSION_NOT_SETUP_HINT='You’re almost there! Please finish setting up Stripe to start accepting Card-Present Payments.' +STRING_CARD_READER_ONBOARDING_STRIPE_EXTENSION_UNSUPPORTED_VERSION_HEADER='Update Stripe' +STRING_CARD_READER_ONBOARDING_STRIPE_EXTENSION_UNSUPPORTED_VERSION_HINT='Outdated version of the WooCommerce Stripe Gateway extension is installed on your store. Please update it to accept In-Person Payments.' +STRING_CARD_READER_ONBOARDING_ACCOUNT_REJECTED_HEADER='In-Person Payments is currently unavailable' +STRING_CARD_READER_ONBOARDING_ACCOUNT_REJECTED_HINT='We are sorry but we can\’t support In-Person Payments for this store.' +STRING_CARD_READER_ONBOARDING_ACCOUNT_UNDER_REVIEW_HEADER='In-Person Payments is currently unavailable' +STRING_CARD_READER_ONBOARDING_ACCOUNT_UNDER_REVIEW_HINT='You\’ll be able to accept In-Person Payments as soon as we finish reviewing your account.' +STRING_CARD_READER_ONBOARDING_ACCOUNT_OVERDUE_REQUIREMENTS_HEADER='Overdue requirements on your account' +STRING_CARD_READER_ONBOARDING_ACCOUNT_OVERDUE_REQUIREMENTS_HINT='You have at least one overdue requirement. You can skip and keep taking payments, but if the requirement is blocking your account, some transactions may be declined until you resolve it.' +STRING_CARD_READER_ONBOARDING_ACCOUNT_OVERDUE_REQUIREMENTS_TAKE_CARE_BUTTON='Resolve now' +STRING_CARD_READER_ONBOARDING_ACCOUNT_PENDING_REQUIREMENTS_HEADER='Your account has pending requirements' +STRING_CARD_READER_ONBOARDING_ACCOUNT_PENDING_REQUIREMENTS_HINT_RE='There\ are\ pending\ requirements\ in\ your\ account\.\ Please\ complete\ those\ requirements\ by\ .*\ to\ keep\ accepting\ In\-Person\ Payments\.' +STRING_CARD_READER_ONBOARDING_ACCOUNT_PENDING_REQUIREMENTS_WITHOUT_DATE_HINT='There are pending requirements in your account. Please complete those requirements to keep accepting In-Person Payments.' +STRING_CARD_READER_ONBOARDING_WCPAY_IN_TEST_MODE_WITH_LIVE_ACCOUNT_HEADER='In-Person Payments is currently unavailable' +STRING_CARD_READER_ONBOARDING_WCPAY_IN_TEST_MODE_WITH_LIVE_ACCOUNT_HINT='In-Person Payments isn\’t available in Test Mode. Please turn it off to continue.' +STRING_CARD_READER_ONBOARDING_CHOOSE_PAYMENT_PROVIDER='Choose your Payment Provider' +STRING_CARD_READER_ONBOARDING_CHOOSE_PLUGIN_HINT='In-Person Payments can be processed through either of these payment providers. Which provider would you like to use?' +STRING_CARD_READER_ONBOARDING_CHOOSE_WCPAYMENT_BUTTON='WooCommerce Payments' +STRING_CARD_READER_ONBOARDING_CHOOSE_STRIPE_BUTTON='Stripe' +STRING_CARD_READER_ONBOARDING_CONFIRM_PAYMENT_METHOD_BUTTON='Confirm Payment Method' +STRING_CARD_READER_ONBOARDING_LEARN_MORE='Learn more about accepting payments with your mobile device and ordering card readers' +STRING_CARD_READER_ONBOARDING_CONTACT_SUPPORT='Need some help? Contact support' +STRING_CARD_READER_ONBOARDING_CONTACT_US='Need some help? Contact us' +STRING_CARD_READER_ONBOARDING_GENERIC_ERROR_HEADER='Unable to verify In-Person Payments for this store' +STRING_CARD_READER_ONBOARDING_GENERIC_ERROR_HINT='We'"'"'re sorry, we were unable to verify In-Person Payments for this store' +STRING_CARD_READER_ONBOARDING_CASH_ON_DELIVERY_DISABLED_ERROR_HEADER='Do you want to add Pay In Person to your web checkout?' +STRING_CARD_READER_ONBOARDING_CASH_ON_DELIVERY_DISABLED_ERROR_HINT='Enabling Pay In Person lets customer pay you for online orders at delivery via cash or card.\n\nOrders can still be created manually without enabling this feature.' +STRING_CARD_READER_ONBOARDING_CASH_ON_DELIVERY_DISABLED_BUTTON='Enable Pay In Person' +STRING_CARD_READER_ONBOARDING_CASH_ON_DELIVERY_ENABLE_FAILURE='Failed to enable cash on delivery. Please try again later.' +STRING_CARD_READER_TUTORIAL_CONNECTED_LABEL='Reader connected' +STRING_CARD_READER_TUTORIAL_CONNECTED_DETAIL='Congrats, you are now able to accept debit and credit card payments!' +STRING_CARD_READER_TUTORIAL_COLLECT_LABEL='Swipe, tap or insert card' +STRING_CARD_READER_TUTORIAL_COLLECT_DETAIL='To collect payments, simply swipe, tap or insert card on reader' +STRING_CARD_READER_TUTORIAL_CHARGED_LABEL='Keep your reader charged' +STRING_CARD_READER_TUTORIAL_CHARGED_DETAIL='Your reader takes about three hours to fully charge' +STRING_CARD_READER_ACCESSIBILITY_READER_IS_CONNECTED='Reader is connected' +STRING_CARD_READER_ACCESSIBILITY_READER_IS_DISCONNECTED='Reader is disconnected' +STRING_NEW_NOTIFICATIONS_RE='.*\ new\ notifications' +STRING_NOTIFICATION_ORDER_TITLE='You have a new order! 🎉' +STRING_NOTIFICATION_REVIEW_TITLE='You have a new review! 🌟' +STRING_CHA_CHING_SOUND_ISSUE_DIALOG_TITLE='Cha-ching sound off' +STRING_CHA_CHING_SOUND_ISSUE_DIALOG_MESSAGE='Turn it back on to hear the '"'"'cha-ching'"'"' with every new sale. Keep in tune with your customers'"'"' orders!' +STRING_CHA_CHING_SOUND_ISSUE_DIALOG_TURN_ON_SOUND='ENABLE SOUND' +STRING_CHA_CHING_SOUND_ISSUE_DIALOG_KEEP_SILENT='KEEP SILENT' +STRING_CHA_CHING_SOUND_SUCCCESS_SNACKBAR='All set! The '"'"'cha-ching'"'"' will now sound for every new order.' +STRING_CHA_CHING_SOUND_SUCCCESS_SNACKBAR_ACTION='TEST SOUND' +STRING_CHA_CHING_SOUND_TEST_NOTIFICATION_TITLE='Test notification' +STRING_CHA_CHING_SOUND_TEST_NOTIFICATION_MESSAGE='This is just a test notification to check the Cha-Ching sound.\nYou can swipe it away.' +STRING_REVIEW_FETCH_ERROR='Error fetching product reviews' +STRING_WC_VIEW_THE_PRODUCT_EXTERNAL='View the product' +STRING_WC_APPROVE='Approve' +STRING_WC_APPROVED='Approved' +STRING_WC_UNAPPROVED='Unapproved' +STRING_WC_SPAM='Spam' +STRING_WC_TRASH='Trash' +STRING_WC_REPLY='Reply' +STRING_WC_LOAD_REVIEW_ERROR='Error loading product review detail' +STRING_WC_MODERATE_REVIEW_ERROR='Error updating product review status' +STRING_WC_REVIEW_TITLE='Review' +STRING_REVIEW_CARD_TRANSITION_NAME_RE='review_card_.*' +STRING_REVIEW_CARD_DETAIL_TRANSITION_NAME='review_card_detail' +STRING_REVIEW_REPLY_SUCCESS='Reply sent!' +STRING_REVIEW_REPLY_FAILURE='There was an error sending the reply' +STRING_REVIEW_LIST_ITEM_TITLE_RE='.*\ left\ a\ review\ on\ .*' +STRING_PRODUCT_REVIEW_LIST_ITEM_TITLE_RE='.*\ left\ a\ review' +STRING_REVIEW_MODERATION_UNDO_RE='Review\ marked\ as\ .*' +STRING_WC_MARK_ALL_READ='Mark all as read' +STRING_WC_MARK_ALL_READ_SUCCESS='All reviews marked as read' +STRING_WC_MARK_ALL_READ_ERROR='Error marking all reviews as read' +STRING_PENDING_REVIEW_LABEL='Pending Review' +STRING_PRODUCT_REVIEW_LIST_UNREAD_REVIEWS_FILTER='Unread reviews only' +STRING_PRODUCT_IMAGE_CONTENT_DESCRIPTION='Product image' +STRING_PRODUCT_ADD_IMAGE_CONTENT_DESCRIPTION='Add image' +STRING_PRODUCT_DETAIL_FETCH_PRODUCT_ERROR='Error fetching product' +STRING_PRODUCT_DETAIL_FETCH_PRODUCT_INVALID_ID_ERROR='Product not found' +STRING_PRODUCT_DETAIL_PRODUCT_NOT_SELECTED='Product not selected' +STRING_PRODUCT_NAME='Title' +STRING_PRODUCT_PURCHASE_NOTE='Purchase note' +STRING_PRODUCT_PROPERTY_EDIT='Edit product' +STRING_PRODUCT_INVENTORY='Inventory' +STRING_PRODUCT_INVENTORY_EMPTY='Add inventory' +STRING_PRODUCT_PRICE='Price' +STRING_PRODUCT_SALE='Sale' +STRING_PRODUCT_PRICE_EMPTY='Add price' +STRING_VARIABLE_PRODUCT_ATTRIBUTES='Variations attributes' +STRING_PRODUCT_ATTRIBUTES='Attributes' +STRING_PRODUCT_ATTRIBUTES_CREATED_TITLE='Attributes created' +STRING_PRODUCT_ATTRIBUTES_CREATED_DESCRIPTION='Now that you'"'"'ve added attributes, you can create your first variation!' +STRING_PRODUCT_ATTRIBUTES_CREATED_GENERATE_ACTION_TEXT='Generate Variation' +STRING_PRODUCT_SHIPPING_EMPTY='Add shipping' +STRING_PRODUCT_REGULAR_PRICE='Regular price' +STRING_PRODUCT_SALE_PRICE='Sale price' +STRING_PRODUCT_SUBSCRIPTION_INTERVAL='Billing interval' +STRING_PRODUCT_SUBSCRIPTION_PERIOD='Period' +STRING_PRODUCT_SCHEDULE_SALE_LABEL='Schedule sale' +STRING_PRODUCT_SCHEDULE_SALE_SUBLABEL='Automatically start and end a sale' +STRING_PRODUCT_SCHEDULE_SALE_FROM_LABEL='From' +STRING_PRODUCT_SCHEDULE_SALE_TO_LABEL='To' +STRING_PRODUCT_SCHEDULE_REMOVE_END_DATE_LINK_LABEL='Remove end date' +STRING_PRODUCT_SALE_DATES='Sale dates' +STRING_PRODUCT_SALE_DATE_FROM_TO_RE='.*\ \-\ .*' +STRING_PRODUCT_SALE_DATE_FROM_RE='From\ .*' +STRING_PRODUCT_SALE_DATE_TO_RE='Until\ .*' +STRING_PRODUCT_TAX_SETTINGS='Tax settings' +STRING_PRODUCT_TAX_STATUS='Tax status' +STRING_PRODUCT_TAX_CLASS='Tax class' +STRING_PRODUCT_TAX_STATUS_NONE='None' +STRING_PRODUCT_TAX_STATUS_TAXABLE='Taxable' +STRING_PRODUCT_TAX_STATUS_SHIPPING='Shipping' +STRING_PRODUCT_SKU='SKU' +STRING_PRODUCT_GLOBAL_UNIQUE_ID='GTIN, UPC, EAN, ISBN' +STRING_PRODUCT_SKU_SUMMARY='Helps to easily identify this product' +STRING_PRODUCT_GLOBAL_UNIQUE_ID_SUMMARY='Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.' +STRING_PRODUCT_STOCK_STATUS='Stock status' +STRING_PRODUCT_TYPE='Product type' +STRING_PRODUCT_CATEGORY='Category' +STRING_PRODUCT_FILTER_DEFAULT='Any' +STRING_PRODUCT_MANAGE_STOCK='Manage stock' +STRING_PRODUCT_SOLD_INDIVIDUALLY='Limit one per order' +STRING_PRODUCT_STOCK_QUANTITY='Stock quantity' +STRING_PRODUCT_BACKORDERS='Backorders' +STRING_PRODUCT_SHIPPING='Shipping' +STRING_PRODUCT_WEIGHT='Weight' +STRING_PRODUCT_LENGTH='Length' +STRING_PRODUCT_WIDTH='Width' +STRING_PRODUCT_HEIGHT='Height' +STRING_PRODUCT_DIMENSIONS='Dimensions' +STRING_PRODUCT_SHIPPING_CLASS='Shipping class' +STRING_PRODUCT_NO_SHIPPING_CLASS='No shipping class' +STRING_PRODUCT_SHIPPING_SETTINGS='Shipping settings' +STRING_PRODUCT_RATINGS_COUNT_RE='\\u2022\ \ .*\ approved\ reviews' +STRING_PRODUCT_RATINGS_COUNT_ONE='\u2022 one approved review' +STRING_PRODUCT_RATINGS_COUNT_ZERO='\u2022 no approved reviews' +STRING_PRODUCT_REVIEWS='Reviews' +STRING_PRODUCT_DOWNLOADABLE_FILES='Downloadable files' +STRING_PRODUCT_CUSTOM_FIELDS='Custom Fields' +STRING_PRODUCT_CUSTOM_FIELDS_DESC='View and edit custom fields' +STRING_PRODUCT_SHARE_DIALOG_TITLE='Share your product' +STRING_PRODUCT_VIEW_IN_STORE='View product on store' +STRING_PRODUCT_STOCK_STATUS_INSTOCK='In stock' +STRING_PRODUCT_STOCK_STATUS_LOW_STOCK='Low stock' +STRING_PRODUCT_STOCK_STATUS_INSTOCK_QUANTIFIED_RE='.*\ in\ stock' +STRING_PRODUCT_STOCK_STATUS_INSTOCK_WITH_VARIATIONS_RE='In\ stock\ \\u2022\ .*\ variations' +STRING_PRODUCT_STOCK_STATUS_OUT_OF_STOCK='Out of stock' +STRING_PRODUCT_STOCK_STATUS_ON_BACKORDER='On backorder' +STRING_PRODUCT_STOCK_STATUS_INSUFFICIENT_STOCK='Insufficient stock' +STRING_PRODUCT_STOCK_COUNT_RE='.*\ in\ stock' +STRING_PRODUCT_BACKORDERS_NO='Do not allow' +STRING_PRODUCT_BACKORDERS_YES='Allow' +STRING_PRODUCT_BACKORDERS_NOTIFY='Allow, but notify customer' +STRING_PRODUCT_LIST_EMPTY='No products yet' +STRING_PRODUCT_LIST_EMPTY_FILTERS='No products found' +STRING_PRODUCT_LIST_FILTERS='Filters' +STRING_PRODUCT_LIST_SORTING_HEADER='Sort by' +STRING_PRODUCT_LIST_SORTING_NEWEST_TO_OLDEST='Date: Newest to oldest' +STRING_PRODUCT_LIST_SORTING_NEWEST_TO_OLDEST_SHORT='Newest' +STRING_PRODUCT_LIST_SORTING_OLDEST_TO_NEWEST='Date: Oldest to newest' +STRING_PRODUCT_LIST_SORTING_OLDEST_TO_NEWEST_SHORT='Oldest' +STRING_PRODUCT_LIST_SORTING_A_TO_Z='Title: A to Z' +STRING_PRODUCT_LIST_SORTING_A_TO_Z_SHORT='A to Z' +STRING_PRODUCT_LIST_SORTING_Z_TO_A='Title: Z to A' +STRING_PRODUCT_LIST_SORTING_Z_TO_A_SHORT='Z to A' +STRING_PRODUCT_LIST_FILTERS_COUNT_RE='Filters\ \(.*\)' +STRING_PRODUCT_LIST_FILTERS_SELECTED_RE='Filters\ \\u2022\ .*' +STRING_PRODUCT_LIST_FILTERS_SHOW_PRODUCTS='Show products' +STRING_PRODUCT_LIST_FILTERS_LIST_ITEM='Selected filter option' +STRING_PRODUCT_LIST_UNSAVED_PRODUCT_UNSELECTED_TITLE_RE='You\ about\ to\ discard\ changes\ to\ .*' +STRING_PRODUCT_LIST_UNSAVED_PRODUCT_UNSELECTED_MESSAGE='Are you sure you want to discard the changes you made to this product?' +STRING_PRODUCT_LIST_FETCH_ERROR='Error fetching products!' +STRING_PRODUCT_SEARCH_HINT='Search products' +STRING_PRODUCT_SEARCH_HINT_ACTIVE_FILTERS='Search filtered products' +STRING_PRODUCT_SEARCH_ALL='All products' +STRING_PRODUCT_SEARCH_SKU='SKU' +STRING_PRODUCT_LIST_SORTING_LIST_ITEM='Selected sorting option' +STRING_PRODUCT_WIP_TITLE='New editing options available' +STRING_PRODUCT_WIP_TITLE_M5='New features available!' +STRING_PRODUCT_VARIANT_LIST_ADD_FIRST_VARIATION='Create your first variation' +STRING_PRODUCT_VARIANT_LIST_EMPTY_ACTION='Add Variation' +STRING_PRODUCT_DESCRIPTION_HINT_NO_TITLE='Describe your product to your future customers…' +STRING_PRODUCT_DESCRIPTION_HINT_WITH_TITLE_RE='Tell\ us\ more\ about\ .*…' +STRING_PRODUCT_DESCRIPTION='Description' +STRING_PRODUCT_DESCRIPTION_EMPTY='Describe your product' +STRING_PRODUCT_SHORT_DESCRIPTION='Short description' +STRING_PRODUCT_SAVE_DIALOG_TITLE='Saving your product' +STRING_PRODUCT_DELETE_DIALOG_TITLE='Deleting product' +STRING_PRODUCT_UPDATE_DIALOG_MESSAGE='Please wait…' +STRING_PRODUCT_DETAIL_UPDATE_PRODUCT_ERROR='Error updating product' +STRING_PRODUCT_DETAIL_SAVE_PRODUCT_SUCCESS='Product saved' +STRING_PRODUCT_DETAIL_UPDATE_PRODUCT_PASSWORD_ERROR='Error updating password' +STRING_PRODUCT_DETAIL_TITLE_HINT='Enter Product Title' +STRING_PRODUCT_DETAIL_PRODUCT_TYPE_HINT_RE='.*\ product' +STRING_PRODUCT_DETAIL_ADD_MORE='Add more details' +STRING_PRODUCT_DETAIL_ADD_VARIATIONS='Add variations' +STRING_PRODUCT_INVENTORY_QUANTITY='Quantity' +STRING_PRODUCT_INVENTORY_QUANTITY_SUMMARY='How many items are in stock' +STRING_PRODUCT_INVENTORY_UPDATE_SKU_ERROR='SKU already in use by another product' +STRING_PRODUCT_INVENTORY_UPDATE_GLOBAL_UNIQUE_ID_ERROR='Please enter only numbers and hyphens' +STRING_PRODUCT_PRICING_UPDATE_SALE_PRICE_ERROR='Sale price must be less than regular price' +STRING_PRODUCT_PRICING_SCHEDULED_SALE_PRICE_ERROR='You must set the sale price if a sale is scheduled' +STRING_PRODUCT_SETTINGS='Product settings' +STRING_PRODUCT_DUPLICATE='Duplicate' +STRING_PRODUCT_DUPLICATE_ERROR='Cannot duplicate product' +STRING_PRODUCT_DUPLICATE_PROGRESS_TITLE='Duplicating your product…' +STRING_PRODUCT_DUPLICATE_PROGRESS_BODY='Please wait while we save a copy of this product to your store' +STRING_PRODUCT_DUPLICATE_COPIED_PRODUCT_NAME_RE='.*\ Copy' +STRING_PRODUCT_DUPLICATE_DISCARD_CHANGES_TITLE='Discard changes and duplicate?' +STRING_PRODUCT_DUPLICATE_DISCARD_CHANGES_MESSAGE='Your unsaved changes will be lost. The duplicate will use the last saved version.' +STRING_PRODUCT_DUPLICATE_DISCARD_CHANGES_ACTION='Discard & duplicate' +STRING_PRODUCT_TRASH='Trash product' +STRING_PRODUCT_CONFIRM_TRASH='Do you want to move this product to the Trash?' +STRING_PRODUCT_TRASH_YES='Move to trash' +STRING_PRODUCT_TRASH_UNDO_SNACKBAR_MESSAGE='Product moved to trash' +STRING_PRODUCT_TRASH_ERROR='Error trashing product' +STRING_PRODUCT_IMAGES_LEARN_MORE_BUTTON='Learn more about uploading images' +STRING_PRODUCT_STATUS='Status' +STRING_PRODUCT_VISIBILITY='Visibility' +STRING_PRODUCT_VISIBILITY_PUBLIC='Public' +STRING_PRODUCT_VISIBILITY_PRIVATE='Private' +STRING_PRODUCT_VISIBILITY_PASSWORD_PROTECTED_HINT='Enter password' +STRING_PRODUCT_VISIBILITY_PASSWORD_PROTECTED='Password protected' +STRING_PRODUCT_VISIBILITY_PASSWORD_REQUIRED='Password is required' +STRING_PRODUCT_CATALOG_VISIBILITY='Catalog visibility' +STRING_PRODUCT_CATALOG_VISIBILITY_VISIBLE='Shop and search results' +STRING_PRODUCT_CATALOG_VISIBILITY_CATALOG='Shop only' +STRING_PRODUCT_CATALOG_VISIBILITY_SEARCH='Search results only' +STRING_PRODUCT_CATALOG_VISIBILITY_HIDDEN='Hidden' +STRING_PRODUCT_FEATURED='Featured product' +STRING_PRODUCT_VISIBILITY_HEADLINE='This setting determines which shop pages products will be listed on.' +STRING_PRODUCT_SLUG='Slug' +STRING_PRODUCT_SLUG_LABEL='This is the URL-friendly version of the product title' +STRING_PRODUCT_ENABLE_REVIEWS='Enable reviews' +STRING_PRODUCT_IS_VIRTUAL='Virtual product' +STRING_PRODUCT_EXTERNAL_EMPTY_LINK='Add product link' +STRING_PRODUCT_EXTERNAL_LINK='Product link' +STRING_PRODUCT_EXTERNAL_LINK_LABEL='Enter the external URL to the product' +STRING_PRODUCT_EXTERNAL_LINK_BUTTON_TEXT='Button text' +STRING_PRODUCT_EXTERNAL_LINK_BUTTON_TEXT_LABEL='This text will be shown on the button linking to the external product' +STRING_PRODUCT_PURCHASE_NOTE_CAPTION='An optional note to send the customer after purchase' +STRING_PRODUCT_MENU_ORDER='Menu order' +STRING_PRODUCT_MENU_ORDER_CAPTION='Determines the product'"'"'s positioning in the catalog. The lower the value of the number, the higher the item will be on the product list. You can also use negative numbers.' +STRING_PRODUCT_VARIATION_ENABLED='Enabled' +STRING_PRODUCT_VARIATION_DISABLED='Disabled' +STRING_PRODUCT_VARIATION_NO_PRICE_SET='No price set' +STRING_PRODUCT_CATEGORIES='Categories' +STRING_PRODUCT_CATEGORY_CLEAR_PARENT='Clear parent category' +STRING_PRODUCT_CATEGORY_LIST_EMPTY_TITLE='Add your first category' +STRING_PRODUCT_CATEGORY_LIST_EMPTY_MESSAGE='Organise your products in categories' +STRING_PRODUCT_ADD_CATEGORY='Add category' +STRING_ADD_PRODUCT_CATEGORY_SUCCESS='Product category added' +STRING_UPDATE_PRODUCT_CATEGORY_SUCCESS='Product category updated' +STRING_DELETE_PRODUCT_CATEGORY_SUCCESS='Product category deleted' +STRING_ADD_PRODUCT_CATEGORY_DUPLICATE='Error creating category - Name already exists on your site.' +STRING_ADD_PRODUCT_CATEGORY_FAILED='Error creating category' +STRING_DELETE_PRODUCT_CATEGORY_FAILED='Error deleting category' +STRING_ADD_PRODUCT_CATEGORY_EMPTY='Enter a category name' +STRING_PRODUCT_CATEGORY_NAME='Category name' +STRING_PRODUCT_CATEGORY_PARENT='Parent category' +STRING_PRODUCT_ADD_CATEGORY_DIALOG_TITLE='Adding category' +STRING_PRODUCT_UPDATE_CATEGORY_DIALOG_TITLE='Updating category' +STRING_PRODUCT_REMOVING_CATEGORY_DIALOG_TITLE='Removing category' +STRING_PRODUCT_ADD_CATEGORY_DIALOG_MESSAGE='Please wait…' +STRING_PRODUCT_TAG_EMPTY='Add tag' +STRING_PRODUCT_TAGS='Tags' +STRING_PRODUCT_TAG_LIST_EMPTY_TITLE='Add your first tag' +STRING_PRODUCT_TAG_LIST_EMPTY_MESSAGE='Organise your products in tags' +STRING_PRODUCT_ADD_TAG_DIALOG_TITLE='Adding tag' +STRING_PRODUCT_ADD_TAG_ERROR='Error occurred when adding tags' +STRING_PRODUCT_TYPE_EDIT='Change product type' +STRING_GROUPED_PRODUCTS='Grouped products' +STRING_GROUPED_PRODUCT_BTN_DELETE='Delete the grouped product' +STRING_GROUPED_PRODUCT_ADD='Add product' +STRING_GROUPED_PRODUCT_EMPTY='Add products to the group' +STRING_PRODUCT_SELECTION_COUNT_RE='.*\ products\ selected' +STRING_PRODUCT_SELECTION_COUNT_SINGLE_RE='.*\ product\ selected' +STRING_PRODUCT_SELECTION_MENU_UPDATE_STATUS='Update status' +STRING_PRODUCT_SELECTION_MENU_UPDATE_PRICE='Update price' +STRING_PRODUCT_SELECTION_MENU_UPDATE_STOCK_STATUS='Update stock status' +STRING_PRODUCT_SELECTION_MENU_SELECT_ALL='Select all' +STRING_PRODUCT_DETAIL_SAVE_AS_DRAFT='Save as draft' +STRING_PRODUCT_DETAIL_LINKED_PRODUCTS='Linked products' +STRING_UPSELLS_LABEL='Upsells' +STRING_UPSELLS_DESC='Products promoted instead of the currently viewed product (ie: more profitable products)' +STRING_CROSS_SELLS_LABEL='Cross-sells' +STRING_CROSS_SELLS_DESC='Products promoted in the cart when current product is selected' +STRING_ADD_PRODUCTS_BUTTON='Add products' +STRING_EDIT_PRODUCTS_BUTTON='Edit products' +STRING_EMPTY_PRODUCT_ADD_PRODUCT_BUTTON='Add product' +STRING_EMPTY_PRODUCT_MESSAGE='Start selling today by adding your first product to the store.' +STRING_PRODUCT_SUBSCRIPTION_EXPIRATION_TITLE='Subscription expiration' +STRING_PRODUCT_SUBSCRIPTION_FREE_TRIAL_TITLE='Subscription free trial' +STRING_PRODUCT_SUBSCRIPTION_FREE_TRIAL_INFO='An optional period of time to wait before charging the first recurring payment. Any sign up fee will still be charged at the outset of the subscription. The trial period can not exceed: 90 days, 52 weeks, 24 months or 5 years.' +STRING_PRODUCT_ADD_ONS_TITLE='Product Add-ons' +STRING_PRODUCT_ADD_ONS_DETAILS_INFO_NOTICE='You can edit product add-ons in the web dashboard.' +STRING_PRODUCT_COUNT_ONE_RE='.*\ product' +STRING_PRODUCT_COUNT_MANY_RE='.*\ products' +STRING_CATEGORY_COUNT_ONE_RE='.*\ category' +STRING_CATEGORY_COUNT_MANY_RE='.*\ categories' +STRING_CROSS_SELL_PRODUCT_COUNT_ONE_RE='.*\ cross\-sell\ product' +STRING_CROSS_SELL_PRODUCT_COUNT_MANY_RE='.*\ cross\-sell\ products' +STRING_UPSELL_PRODUCT_COUNT_ONE_RE='.*\ upsell\ product' +STRING_UPSELL_PRODUCT_COUNT_MANY_RE='.*\ upsell\ products' +STRING_PRODUCT_DETAIL_BACKGROUND_IMAGE_UPLOAD='Image uploading will continue in the background' +STRING_PRODUCT_CARD_DETAIL_TRANSITION_NAME='product_card_detail' +STRING_PRODUCT_CATEGORY_SELECTOR_TITLE='Select categories' +STRING_PRODUCT_CATEGORY_SELECTOR_EMPTY_STATE='No product categories found' +STRING_PRODUCT_CATEGORY_SELECTOR_SELECT_BUTTON_TITLE_DEFAULT_RE='Select\ .*\ Categories' +STRING_PRODUCT_CATEGORY_SELECTOR_SELECT_BUTTON_TITLE_ONE='Select 1 category' +STRING_PRODUCT_CATEGORY_SELECTOR_CHECK_CONTENT_DESCRIPTION='Click to uncheck' +STRING_PRODUCT_CATEGORY_SELECTOR_CLEAR_SELECTION='Clear selection' +STRING_PRODUCT_CATEGORY_SELECTOR_SEARCH_HINT='Search Categories' +STRING_PRODUCT_CATEGORY_SELECTOR_LOADING_FAILED='Loading product categories failed' +STRING_PRODUCT_CATEGORY_SELECTOR_SEARCH_FAILED='Searching product categories failed' +STRING_PRODUCT_CREATION_AI_ENTRY_SHEET_HEADER='Add a product' +STRING_PRODUCT_CREATION_AI_ENTRY_SHEET_AI_OPTION_TITLE='Create a product with AI' +STRING_PRODUCT_CREATION_AI_ENTRY_SHEET_AI_OPTION_SUBTITLE_V2='Let us generate product details for you' +STRING_PRODUCT_CREATION_AI_ENTRY_SHEET_MANUAL_OPTION_TITLE='Add manually' +STRING_PRODUCT_CREATION_AI_ENTRY_SHEET_MANUAL_OPTION_SUBTITLE='Add a product and the details manually' +STRING_PRODUCT_CREATION_AI_ENTRY_SHEET_LEARN_MORE='Powered by AI. Learn more.' +STRING_AI_PRODUCT_CREATION_PRODUCT_PROMPT_TITLE='Starting information' +STRING_AI_PRODUCT_CREATION_PRODUCT_PROMPT_SUBTITLE='Tell us about your product, what it is and what makes it unique, then let the AI work its magic.' +STRING_AI_PRODUCT_CREATION_PROMPT_PLACEHOLDER='For example: Black cotton t-shirt, soft fabric, durable stitching, unique design' +STRING_AI_PRODUCT_CREATION_READ_TEXT_FROM_PHOTO_BUTTON='Read text from product photo' +STRING_PRODUCT_CREATION_AI_PREVIEW_TITLE='Preview' +STRING_PRODUCT_CREATION_AI_PREVIEW_SUBTITLE='You can edit or regenerate your product details before saving.' +STRING_PRODUCT_CREATION_AI_PREVIEW_NAME_DESCRIPTION_SECTIONS='Name, Summary & Description' +STRING_PRODUCT_CREATION_AI_PREVIEW_DETAILS_SECTION='Details' +STRING_PRODUCT_CREATION_AI_PREVIEW_VARIANT_SELECTOR_RE='Option\ .*\ of\ .*' +STRING_PRODUCT_CREATION_AI_PREVIEW_UNDO_EDITS='Undo edits' +STRING_PRODUCT_CREATION_AI_PREVIEW_GENERATE_AGAIN='Generate Again' +STRING_PRODUCT_CREATION_AI_SELECT_PREVIOUS_OPTION='Select previous option' +STRING_PRODUCT_CREATION_AI_SELECT_NEXT_OPTION='Select next option' +STRING_PRODUCT_CREATION_AI_TONE_TITLE='Tone and voice' +STRING_PRODUCT_CREATION_AI_TONE_CASUAL='Casual' +STRING_PRODUCT_CREATION_AI_TONE_FORMAL='Formal' +STRING_PRODUCT_CREATION_AI_TONE_FLOWERY='Flowery' +STRING_PRODUCT_CREATION_AI_TONE_CONVINCING='Convincing' +STRING_PRODUCT_CREATION_AI_GENERATION_FAILURE_MESSAGE='Product generation failed. Please try again' +STRING_PRODUCT_CREATION_PACKAGE_PHOTO_NO_TEXT_DETECTED='No text detected. Please select another packaging photo or enter product details manually.' +STRING_PRODUCT_FILTER_EXPLORE_PLUGIN='Explore!' +STRING_SCAN_BARCODE_TO_UPDATE_INVENTORY_MENU_ITEM_TITLE='Scan barcode to update inventory' +STRING_AI_PRODUCT_CREATION_GENERATE_DETAILS_BUTTON='Generate Product Details' +STRING_AI_PRODUCT_CREATION_VIEW_IMAGE='View photo' +STRING_AI_PRODUCT_CREATION_REPLACE_IMAGE='Replace photo' +STRING_AI_PRODUCT_CREATION_REMOVE_IMAGE='Remove photo' +STRING_AI_PRODUCT_CREATION_IMAGE_SELECTED='Photo selected' +STRING_AI_PRODUCT_CREATION_IMAGE_SCAN_SUBTITLE='Add text scanned from a photo' +STRING_AI_PRODUCT_CREATION_IMAGE_SELECTED_SUBTITLE='Photo text added to starting info' +STRING_AI_PRODUCT_CREATION_SCANNING_IMAGE='Scanning image' +STRING_AI_PRODUCT_CREATION_SCANNING_PHOTO_ERROR='Error while trying to scan the text from the photo. Please try again' +STRING_AI_PRODUCT_CREATION_PROMPT_SUGGESTION_INITIAL='Add your product'"'"'s name and key features, benefits, or details to help it get found online.' +STRING_AI_PRODUCT_CREATION_PROMPT_SUGGESTION_ADD_MORE_DETAILS='Add more details. The more details you provide, the better your generated details will be.' +STRING_AI_PRODUCT_CREATION_PROMPT_SUGGESTION_GETTING_BETTER='Getting better. Can you describe the fit and any distinctive features of the item?' +STRING_AI_PRODUCT_CREATION_PROMPT_SUGGESTION_ALMOST_THERE='Great prompt! Where was it made?' +STRING_AI_PRODUCT_CREATION_PROMPT_SUGGESTION_GREAT_PROMPT='Great prompt! You'"'"'ve given us enough to work with, but you may add more detail to make it even better.' +STRING_AI_PRODUCT_CREATION_ERROR_MEDIA_UPLOAD='Failed to upload the selected product image.' +STRING_AI_PRODUCT_CREATION_PHOTO_REMOVED='Photo removed' +STRING_BOTTOM_SHEET_ADD_DETAILS_LIST_HEADER='Add more details' +STRING_BOTTOM_SHEET_SHIPPING_DESC='Add weight and dimensions' +STRING_BOTTOM_SHEET_CATEGORIES_DESC='Organise your products into related groups' +STRING_BOTTOM_SHEET_TAGS_DESC='Make your products easier to find with tags' +STRING_BOTTOM_SHEET_SHORT_DESCRIPTION_DESC='A brief excerpt about your product' +STRING_BOTTOM_SHEET_LINKED_PRODUCTS_DESC='Increase sales with upsells and cross-sells' +STRING_VARIATION_DETAIL_FETCH_VARIATION_ERROR='Error fetching variation' +STRING_VARIATION_DETAIL_UPDATE_VARIATION_ERROR='Error updating variation' +STRING_VARIATION_DETAIL_UPDATE_VARIATION_IMAGE_ERROR='Sorry, image removal on product variations is supported in WooCommerce 4.7 or greater.' +STRING_VARIATION_DETAIL_UPDATE_PRODUCT_SUCCESS='Variation updated' +STRING_VARIATION_DETAIL_PRICE_WARNING='Variations without price won'"'"'t be shown in your store' +STRING_VARIATION_DETAIL_IMAGE_ADD='Add a variation image' +STRING_VARIATION_LIST_GENERATE_NEW_VARIATION='Generate new variation' +STRING_VARIATION_LIST_ADD='Add variation' +STRING_VARIATION_CREATED_TITLE='Variation Created' +STRING_VARIATION_CREATE_DIALOG_TITLE='Generating variation' +STRING_VARIATION_LOADING_DIALOG_TITLE='Fetching variations…' +STRING_VARIATION_CONFIRM_DELETE='Do you want to delete this Variation?' +STRING_VARIATION_LIST_MENU_BULK_UPDATE='Bulk update…' +STRING_VARIATIONS_BULK_LIMIT_EXCEEDED_BUTTON_OK='OK' +STRING_VARIATION_ADD_NEW='Add new variation' +STRING_VARIATION_ADD_NEW_DESCRIPTION='Create one new variation. Manually set which attributes belong to the variable product.' +STRING_VARIATION_ADD_ALL='Generate all variations' +STRING_VARIATION_ADD_ALL_DESCRIPTION='Creates variations for all combinations of your attributes.' +STRING_VARIATIONS_BULK_UPDATE_DIALOG_TITLE='Bulk update' +STRING_VARIATIONS_BULK_UPDATE_DIALOG_SUBTITLE='Choose a value to update' +STRING_VARIATIONS_BULK_UPDATE_DIALOG_PRICE_SECTION_TITLE='Price' +STRING_VARIATIONS_BULK_UPDATE_DIALOG_REGULAR_PRICE_LABEL='Regular price' +STRING_VARIATIONS_BULK_UPDATE_DIALOG_SALE_PRICE_LABEL='Sale price' +STRING_VARIATIONS_BULK_UPDATE_DIALOG_NONE='None' +STRING_VARIATIONS_BULK_UPDATE_DIALOG_MIXED='Mixed' +STRING_VARIATIONS_BULK_UPDATE_PRICE_INFO_RE='Price\ will\ be\ updated\ for\ .*\ variations' +STRING_VARIATIONS_BULK_UPDATE_CURRENT_PRICE_RE='Current\ price\ is\ .*' +STRING_VARIATIONS_BULK_UPDATE_CURRENT_PRICES_MIXED='Current prices are mixed' +STRING_VARIATIONS_BULK_UPDATE_ERROR='Something went wrong. Please try again.' +STRING_VARIATIONS_BULK_UPDATE_REGULAR_PRICES_SUCCESS='Updated Regular Prices.' +STRING_VARIATIONS_BULK_UPDATE_SALE_PRICES_SUCCESS='Updated Sale Prices.' +STRING_VARIATIONS_BULK_UPDATE_SALE_PRICES_DIALOG_TITLE='Updating sale prices' +STRING_VARIATIONS_BULK_UPDATE_REGULAR_PRICES_DIALOG_TITLE='Updating regular prices' +STRING_VARIATIONS_BULK_UPDATE_WARNING_TITLE='Bulk update limit exceeded' +STRING_VARIATIONS_BULK_UPDATE_WARNING_MESSAGE='Currently bulk update is supported for 100 variations maximum.' +STRING_VARIATIONS_BULK_UPDATE_DIALOG_INVENTORY_SECTION_TITLE='Inventory' +STRING_VARIATIONS_BULK_UPDATE_STOCK_QUANTITY_INFO_RE='Stock\ quantity\ will\ be\ updated\ for\ .*\ variations' +STRING_VARIATIONS_BULK_UPDATE_STOCK_QUANTITY_DIALOG_TITLE='Updating stock quantity' +STRING_VARIATIONS_BULK_UPDATE_CURRENT_STOCK_QUANTITY_RE='Current\ stock\ quantity\ is\ .*' +STRING_VARIATIONS_BULK_UPDATE_CURRENT_STOCK_QUANTITY_MIXED='Current stock quantity is mixed' +STRING_VARIATIONS_BULK_UPDATE_STOCK_QUANTITY_SUCCESS='Updated stock quantity' +STRING_VARIATIONS_BULK_CREATION_WARNING_TITLE='Generation limit exceeded' +STRING_VARIATIONS_BULK_CREATION_WARNING_MESSAGE_RE='Currently\ creation\ is\ supported\ for\ .*\ variations\ maximum\.\ Generating\ variations\ for\ this\ product\ would\ create\ .*\ variations\.' +STRING_VARIATIONS_BULK_CREATION_CONFIRMATION_TITLE='Generate all variations?' +STRING_VARIATIONS_BULK_CREATION_CONFIRMATION_MESSAGE_RE='This\ will\ create\ a\ new\ variation\ for\ each\ and\ every\ possible\ combination\ of\ variation\ attributes\ \(.*\ variations\)\.' +STRING_VARIATIONS_BULK_CREATION_PROGRESS_TITLE='Generating variations' +STRING_VARIATIONS_BULK_CREATION_NO_CANDIDATES_TITLE='No variations to generate' +STRING_VARIATIONS_BULK_CREATION_NO_CANDIDATES_MESSAGE='All variations are already generated.' +STRING_PRODUCT_IMAGES_TITLE='Photos' +STRING_PRODUCT_ADD_PHOTOS='Add photos' +STRING_PRODUCT_ADD_PHOTO='Add photo' +STRING_PRODUCT_REPLACE_PHOTO='Replace photo' +STRING_PRODUCT_REMOVE_PHOTO='Remove photo' +STRING_PRODUCT_OPEN_UPLOAD_SCREEN='View recent upload failures' +STRING_PRODUCT_COVER_PHOTO_TAG='Cover' +STRING_PRODUCT_IMAGE_ADD='Add a product image' +STRING_PRODUCT_IMAGE_SERVICE_ERROR_UPLOADING='Error uploading product image' +STRING_PRODUCT_IMAGE_REMOVE_CONFIRMATION='Are you sure you want to remove this image?' +STRING_REMOVE_BACKGROUND='Remove background with AI' +STRING_REMOVE_BACKGROUND_TITLE='Image scene cleaner ✨' +STRING_REMOVE_BACKGROUND_IMAGE_LOAD_ERROR='Failed to load image' +STRING_REMOVE_BACKGROUND_NO_SUBJECT_DETECTED='No clear subject detected. Try with a different image.' +STRING_SAVE_PROCESSED_IMAGE_ERROR='Failed to save image' +STRING_SAVE_PROCESSED_IMAGE_SUCCESS='Image saved successfully' +STRING_DISCARD_CHANGES_QUESTION='Discard changes?' +STRING_CHANGES_NOT_SAVED_MESSAGE='Your changes are not saved' +STRING_SAVE_COPY='Save copy' +STRING_PRODUCT_IMAGES_UPLOADING_SINGLE_NOTIF_MESSAGE='Uploading image…' +STRING_PRODUCT_IMAGES_UPLOADING_MULTI_NOTIF_MESSAGE_RE='Uploading\ images….*\ of\ .*' +STRING_PRODUCT_IMAGES_UPLOAD_CHANNEL_TITLE='Uploads' +STRING_IMAGE_SOURCE_TITLE='Select an upload method' +STRING_IMAGE_SOURCE_DEVICE_CHOOSER='Choose from device' +STRING_IMAGE_SOURCE_DEVICE_CAMERA='Take a photo' +STRING_IMAGE_SOURCE_WP_MEDIA_LIBRARY='WordPress media library' +STRING_IMAGE_SOURCE_PRODUCT_IMAGES='Choose an existing product photo' +STRING_MEDIA_PICKER_DIALOG_TITLE='Select Media Source' +STRING_WPMEDIA_PICKER_TITLE='WordPress media library' +STRING_PRODUCT_IMAGES_IMAGE_LIMIT_WARNING='Only one photo can be displayed per product variation' +STRING_PRODUCT_IMAGES_DRAG_AND_DROP_TO_REORDER='Drag and drop to re-order photos. The first photo will be set as the cover.' +STRING_PRODUCT_IMAGES_VALIDATE_DRAG_AND_DROP='Validate' +STRING_PRODUCT_IMAGE_SERVICE_ERROR_MEDIA_NULL='Media could not be found' +STRING_PRODUCT_IMAGE_SERVICE_ERROR_UPLOADING_SINGLE_RE='.*\ file\ couldn'"'"'t\ be\ uploaded' +STRING_PRODUCT_IMAGE_SERVICE_ERROR_UPLOADING_MULTIPLE_RE='.*\ files\ couldn'"'"'t\ be\ uploaded' +STRING_PRODUCT_IMAGES_ERROR_DETAIL_TITLE_RE='Upload\ details\ \(.*\)' +STRING_PRODUCT_UPDATE_NOTIFICATION_RE='Updating\ product\ .*' +STRING_PRODUCT_UPDATE_SUCCESS_NOTIFICATION_TITLE='Product Updated' +STRING_PRODUCT_UPDATE_SUCCESS_NOTIFICATION_CONTENT_RE='.*\ images\ have\ been\ added\ to\ the\ product\ .*' +STRING_PRODUCT_UPDATE_FAILURE_NOTIFICATION_RE='Updating\ product\ .*\ failed' +STRING_PRODUCT_UPLOAD_ERROR_TITLE='Image upload errors' +STRING_GET_THE_WORD_OUT='Let'"'"'s get the word out' +STRING_SHARE_YOUR_STORE_MESSAGE='Share your store on social media and by email with your contacts.' +STRING_EMPTY_ORDER_LIST_TITLE='Waiting for your first order' +STRING_EMPTY_ORDER_LIST_MESSAGE='Explore how you can increase your store sales.' +STRING_EMPTY_REVIEW_LIST_TITLE='Get your first reviews' +STRING_EMPTY_REVIEW_LIST_MESSAGE='Capture high-quality product reviews for your store.' +STRING_EMPTY_REVIEW_FILTERED_LIST_TITLE='No unread product reviews' +STRING_EMPTY_REVIEW_FILTERED_LIST_MESSAGE='Try disabling the unread filter to see all your product reviews' +STRING_ORDERS_EMPTY_MESSAGE_FOR_FILTERED_ORDERS='We'"'"'re sorry, we couldn'"'"'t find any orders' +STRING_EMPTY_MESSAGE_WITH_SEARCH_RE='We'"'"'re\ sorry,\ we\ couldn'"'"'t\ find\ results\ for\ ".*"' +STRING_EMPTY_MESSAGE_WITH_SEARCH_GUEST='Looking for orders placed by guest customers?' +STRING_EMPTY_SEARCH_GUEST_ORDERS_BUTTON='Show guest orders' +STRING_EMPTY_MESSAGE_WITH_FILTERS='We'"'"'re sorry, no products match the selected filters"' +STRING_SETTINGS='Settings' +STRING_PRIVACY_SETTINGS='Privacy settings' +STRING_SEND_FEEDBACK='Send feedback' +STRING_EXPERIMENTAL_FEATURES='Experimental features' +STRING_SETTINGS_SIGNOUT='Log out' +STRING_SETTINGS_CLOSE_ACCOUNT='Close Account' +STRING_SETTINGS_CLOSE_ACCOUNT_DIALOG_TITLE='Confirm Close Account' +STRING_SETTINGS_CLOSE_ACCOUNT_DIALOG_DESCRIPTION='To confirm, please re-enter your username before closing' +STRING_SETTINGS_CLOSE_ACCOUNT_DIALOG_CONFIRM_BUTTON='Permanently Close Account' +STRING_SETTINGS_CLOSE_ACCOUNT_DIALOG_LOADING_TITLE='Closing account…' +STRING_SETTINGS_CLOSE_ACCOUNT_ERROR_DIALOG_TITLE='Couldn'"'"'t close account' +STRING_SETTINGS_CLOSE_ACCOUNT_GENERIC_ERROR_DESCRIPTION='An error occurred while attempting to close your account.' +STRING_SETTINGS_CLOSE_ACCOUNT_ACTIVE_STORES_ERROR_DESCRIPTION='This account cannot be closed while it has active stores.' +STRING_SETTINGS_CLOSE_ACCOUNT_DIALOG_CONTACT_SUPPORT_BUTTON='Contact Support' +STRING_SETTINGS_PREFERENCES='Preferences' +STRING_SETTINGS_CONFIRM_LOGOUT_RE='Are\ you\ sure\ you\ want\ to\ logout\ from\ the\ account\ .*\?' +STRING_SETTINGS_CONFIRM_LOGOUT_SITE_CREDENTIALS='Are you sure you want to log out of your account?' +STRING_SETTINGS_LOGOUT_DIALOG_MESSAGE='Logging you out.' +STRING_SETTINGS_ENABLE_BETA_FEATURE_FAILED_SNACKBAR_TEXT='Sorry, we couldn'"'"'t change this feature setting right now' +STRING_SETTINGS_ENABLE_PRODUCT_ADDONS_TEASER_TITLE='View Add-ons' +STRING_SETTINGS_ENABLE_PRODUCT_ADDONS_TEASER_MESSAGE='Test out viewing Order Add-ons as we get ready to launch' +STRING_SETTINGS_ENABLE_WOO_POS_LOCAL_CATALOG_TITLE='POS Local Catalog' +STRING_SETTINGS_ENABLE_WOO_POS_LOCAL_CATALOG_MESSAGE='Store your product catalog on this device for faster search, barcode scanning, and loading times.' +STRING_SETTINGS_PRIVACY_POLICY='Read privacy policy' +STRING_SETTINGS_PRIVACY_STATEMENT='We value your privacy. Your personal data is used to optimize our mobile apps, improve security, conduct analytics, and enhance your user experience.' +STRING_SETTINGS_TRACKING_HEADER='Tracking' +STRING_SETTINGS_TRACKING_ANALYTICS='Analytics' +STRING_SETTINGS_TRACKING_ANALYTICS_DESCRIPTION='Allow us to optimize performance by collecting information on how users interact with our mobile apps.' +STRING_SETTINGS_TRACKING_ANALYTICS_ERROR_FETCH='There was an error fetching your privacy settings' +STRING_SETTINGS_TRACKING_ANALYTICS_ERROR_UPDATE='There was an error updating your privacy settings' +STRING_SETTINGS_CRASH_REPORTING_ERROR_UPDATE='There was an error updating your crash reporting setting' +STRING_SETTINGS_MORE_PRIVACY_OPTIONS_HEADER='More privacy options' +STRING_SETTINGS_WEB_OPTIONS='Web Options' +STRING_SETTINGS_WEB_OPTIONS_DESCRIPTION='More privacy options available for woocommerce.com users. Check here to learn more.' +STRING_SETTINGS_USAGE_TRACKING='Usage Tracking' +STRING_SETTINGS_USAGE_TRACKING_DESCRIPTION='Learn more about the data we collect about your store and your options to control this data sharing.' +STRING_SETTINGS_PRIVACY_HEADER='Privacy' +STRING_SETTINGS_PRIVACY_COOKIES_POLICES='Privacy and Cookies Polices' +STRING_SETTINGS_PRIVACY_COOKIES_POLICES_DESCRIPTION='Learn more about our privacy and cookies policies.' +STRING_SETTINGS_REPORTS_HEADER='Reports' +STRING_SETTINGS_REPORTS_REPORT_CRASHES='Report Crashes' +STRING_SETTINGS_REPORTS_REPORT_CRASHES_DESCRIPTION='To help us improve the app’s performance and fix the occasional bug, enable automatic crash report.' +STRING_PRIVACY_BANNER_DESCRIPTION='Your privacy is critically important to us and always has been. We use, store, and process your personal data to optimize our app (and your experience) in various ways. Some uses of your data we absolutely need in order to make things work, and others you can customize from your Settings.' +STRING_PRIVACY_BANNER_TITLE='Manage privacy' +STRING_PRIVACY_BANNER_ANALYTICS='Analytics' +STRING_PRIVACY_BANNER_ANALYTICS_DESCRIPTION='Allow us to optimize performance by collecting information on how users interact with our mobile apps.' +STRING_PRIVACY_BANNER_SETTINGS='Settings' +STRING_PRIVACY_BANNER_SAVE='Save' +STRING_PRIVACY_BANNER_ERROR_SAVE='There was an error saving your privacy choices.' +STRING_SETTINGS_POLICIES_PRIVACY_POLICY='Privacy Policy' +STRING_SETTINGS_POLICIES_PRIVACY_POLICY_DESCRIPTION='Your information helps us to improve our products, marketing, and personalize your experience on WooCommerce.' +STRING_SETTINGS_POLICIES_COOKIE_POLICY='Cookie Policy' +STRING_SETTINGS_POLICIES_COOKIE_POLICY_DESCRIPTION='Our cookie policy explains how we and others use cookies and how you can manage them.' +STRING_SETTINGS_FOOTER_RE='Made\ with\ love\ by\ Automattic\.\ .*' +STRING_SETTINGS_HIRING='We'"'"'re hiring!' +STRING_SETTINGS_STORE='Store settings' +STRING_SETTINGS_CARD_READER_MANUALS='Card Reader Manuals' +STRING_SETTINGS_INSTALL_JETPACK='Install Jetpack' +STRING_SETTINGS_NOTIFS='Notifications' +STRING_SETTINGS_PUSH_NOTIFICATIONS='Push notifications' +STRING_SETTINGS_STORE_NAME='Store name' +STRING_SETTINGS_ENABLE_PUSH_NOTIFICATIONS='Enable Push Notifications' +STRING_SETTINGS_NOTIFS_DEVICE='Manage notifications' +STRING_SETTINGS_NOTIFS_DEVICE_DETAIL='Sounds, urgency, and notification dot' +STRING_SETTINGS_NOTIFS_APP_NOTIFICATIONS_DISABLED_WARNING='Turn on notifications in your device settings to receive store notifications.' +STRING_SETTINGS_NOTIFS_CHANNEL_DISABLED_SUBTITLE='Turn on in device settings' +STRING_SETTINGS_NOTIFS_ENABLE_TITLE='Enable notifications' +STRING_SETTINGS_NOTIFS_ERROR_FETCH='There was an error fetching your notification settings.' +STRING_SETTINGS_NOTIFS_ERROR_UPDATE='There was an error updating your notification settings.' +STRING_SETTINGS_NOTIFS_ENABLE_CHACHING_SOUND='Enable Cha-Ching sound' +STRING_SETTINGS_NOTIFS_ENABLE_CHACHING_SOUND_DESCRIPTION='Orders notifications sound has been disabled, turn it back on to hear the '"'"'cha-ching'"'"' with every new sale.' +STRING_SETTINGS_NOTIFS_RESTORE_CHACHING_SOUND_DESCRIPTION='Orders notifications sound has been modified, use this button to restore the '"'"'cha-ching'"'"' sound.' +STRING_SETTINGS_NOTIFS_NEW_ORDERS='New orders' +STRING_SETTINGS_NOTIFS_NEW_ORDERS_SUBTITLE='All orders' +STRING_SETTINGS_NOTIFS_NEW_ORDERS_ENABLE_DESCRIPTION='Get notified when an order is placed in your store.' +STRING_SETTINGS_NOTIFS_NOTIFY_ME_FOR='Notify me for' +STRING_SETTINGS_NOTIFS_NEW_ORDERS_ALL_TITLE='All new orders' +STRING_SETTINGS_NOTIFS_NEW_ORDERS_ALL_DESCRIPTION='Ping for every order, regardless of value.' +STRING_SETTINGS_NOTIFS_NEW_ORDERS_HIGH_VALUE_TITLE='Only high-value orders' +STRING_SETTINGS_NOTIFS_NEW_ORDERS_HIGH_VALUE_SUBTITLE_RE='Orders\ over\ .*' +STRING_SETTINGS_NOTIFS_NEW_ORDERS_HIGH_VALUE_DESCRIPTION='Filter to orders above your minimum value.' +STRING_SETTINGS_NOTIFS_NEW_ORDERS_THRESHOLD_RE='Minimum\ value\ \(.*\)' +STRING_SETTINGS_NOTIFS_STOCK='Stock' +STRING_SETTINGS_NOTIFS_STOCK_SUBTITLE='All stock alerts' +STRING_SETTINGS_NOTIFS_STOCK_NO_ALERTS_SUBTITLE='No alerts' +STRING_SETTINGS_NOTIFS_STOCK_TWO_SUBTITLE_RE='.*\ and\ .*' +STRING_SETTINGS_NOTIFS_STOCK_ENABLE_TITLE='Enable stock notifications' +STRING_SETTINGS_NOTIFS_STOCK_ENABLE_DESCRIPTION='Get notified when product stock changes need your attention.' +STRING_SETTINGS_NOTIFS_STOCK_LOW_STOCK_TITLE='Low stock' +STRING_SETTINGS_NOTIFS_STOCK_LOW_STOCK_DESCRIPTION='When a product variant reaches its low stock threshold.' +STRING_SETTINGS_NOTIFS_STOCK_LOW_STOCK_THRESHOLD_RE='Products\ can\ use\ their\ own\ threshold\ or\ the\ store\-wide\ threshold\ of .*\.\ Edit\ store\-wide\ threshold' +STRING_SETTINGS_NOTIFS_STOCK_LOW_STOCK_THRESHOLD_UNAVAILABLE='Products can use their own threshold or the store-wide threshold. View store-wide threshold to see the current value.' +STRING_SETTINGS_NOTIFS_STOCK_LOW_STOCK_THRESHOLD_ERROR='Unable to load store-wide threshold' +STRING_SETTINGS_NOTIFS_STOCK_OUT_OF_STOCK_TITLE='Out of stock' +STRING_SETTINGS_NOTIFS_STOCK_OUT_OF_STOCK_DESCRIPTION='When a product variant hits zero.' +STRING_SETTINGS_NOTIFS_STOCK_BACKORDER_TITLE='On backorder' +STRING_SETTINGS_NOTIFS_STOCK_BACKORDER_DESCRIPTION='When a customer orders an item that'"'"'s currently out of stock.' +STRING_SETTINGS_NOTIFS_NEW_REVIEWS='New reviews' +STRING_SETTINGS_NOTIFS_NEW_REVIEWS_SUBTITLE='All reviews' +STRING_SETTINGS_NOTIFS_NEW_REVIEWS_ENABLE_DESCRIPTION='Get notified when a review is left for your store.' +STRING_SETTINGS_NOTIFS_NEW_REVIEWS_ALL_TITLE='All new reviews' +STRING_SETTINGS_NOTIFS_NEW_REVIEWS_ALL_DESCRIPTION='Ping for every review.' +STRING_SETTINGS_NOTIFS_NEW_REVIEWS_RATING_FILTER_TITLE='Only low-rated reviews' +STRING_SETTINGS_NOTIFS_NEW_REVIEWS_RATING_FILTER_DESCRIPTION='Filter to reviews at or below your chosen rating.' +STRING_SETTINGS_NOTIFS_NEW_REVIEWS_SELECTED_RATING_RE='.*\ stars\ and\ below' +STRING_SETTINGS_NOTIFS_NEW_REVIEWS_SELECTED_RATING_ONE_RE='.*\ star\ and\ below' +STRING_SETTINGS_IMAGE_OPTIMIZATION_TITLE='Image optimization' +STRING_SETTINGS_IMAGE_OPTIMIZATION_MESSAGE='Resize and compress images for faster uploading' +STRING_SETTINGS_ABOUT='About the app' +STRING_SETTINGS_LICENSES='Open source licenses' +STRING_SETTINGS_WHATS_NEW='What'"'"'s New in WooCommerce' +STRING_SETTINGS_WHATS_NEW_ICON_DESCRIPTION='New feature icon image' +STRING_FEATURE_ANNOUNCEMENT_DISMISS='Not Now' +STRING_SETTINGS_APP_THEME_TITLE='Appearance' +STRING_SETTINGS_APP_THEME_OPTION_LIGHT='Light' +STRING_SETTINGS_APP_THEME_OPTION_DARK='Dark' +STRING_SETTINGS_APP_THEME_OPTION_SYSTEM='System default' +STRING_SETTINGS_APP_THEME_OPTION_BATTERY_SAVER='Set by Battery Saver' +STRING_SETTINGS_ABOUT_RECOMMEND_APP_SUBJECT='WooCommerce' +STRING_SETTINGS_ABOUT_RECOMMEND_APP_MESSAGE_RE='Hey!\ Here\ is\ a\ link\ to\ download\ the\ WooCommerce\ app\.\ I'"'"'m\ really\ enjoying\ it\ and\ thought\ you\ might\ too\.\ .*' +STRING_DEV_OPTIONS='Developer Options' +STRING_DEV_FEATURE_FLAGS='Feature Flags' +STRING_RESTART='Restart' +STRING_SETTINGS_ACCOUNT='Account Settings' +STRING_SETTINGS_THEMES='Themes' +STRING_SETTINGS_TROUBLESHOOT_CONNECTION='Troubleshoot Connection' +STRING_SETTINGS_PLUGINS='Plugins' +STRING_SETTINGS_OPTION_INSTALLED_PLUGINS='Installed Plugins' +STRING_SETTINGS_WOO_PLUGIN_VERSION='WooCommerce Version' +STRING_PLUGIN_STATE_UP_TO_DATE='Up-to-date' +STRING_PLUGIN_STATE_UPDATE_AVAILABLE_RE='Update\ available\ \(.*\)' +STRING_PLUGIN_STATE_INACTIVE='Inactive' +STRING_PLUGINS_ERROR_MESSAGE='An error occurred while loading installed plugins' +STRING_SETTINGS_ENABLE_JETPACK_APP_PASSWORDS_TITLE='Application Passwords' +STRING_SETTINGS_ENABLE_JETPACK_APP_PASSWORDS_MESSAGE='Enable application passwords to let the app directly fetch data from your WooCommerce site instead of doing so through Jetpack connection.' +STRING_ENABLE_CARD_READER='Enable Simulated Card Reader' +STRING_UPDATE_SIMULATED_READER='Update Simulated Card Reader' +STRING_ALWAYS_UPDATE_READER='Always' +STRING_NEVER_UPDATE_READER='Never' +STRING_RANDOMLY_UPDATE_READER='Randomly' +STRING_TOGGLE_OPTION_CHECKED='option checked' +STRING_TOGGLE_OPTION_NOT_CHECKED='option not checked' +STRING_SIMULATED_READER_TOAST='Simulated Card Reader has been disabled' +STRING_SUPPORT_HELP='Help & support' +STRING_INVALID_EMAIL_MESSAGE='Your email address isn'"'"'t valid' +STRING_A8C_EMAIL_MESSAGE='Please use a non-Automattic email to submit a support ticket' +STRING_SUPPORT_IDENTITY_INPUT_DIALOG_ENTER_EMAIL_AND_NAME='To continue please enter your email address and name' +STRING_SUPPORT_IDENTITY_INPUT_DIALOG_ENTER_EMAIL='Please enter your email address' +STRING_SUPPORT_IDENTITY_INPUT_DIALOG_EMAIL_LABEL='Email' +STRING_SUPPORT_IDENTITY_INPUT_DIALOG_NAME_LABEL='Name' +STRING_SUPPORT_SUBTITLE='How can we help?' +STRING_SUPPORT_HELP_CENTER='Help Center' +STRING_SUPPORT_FAQ_DETAIL='Get answers to questions you have' +STRING_SUPPORT_CONTACT='Contact Support' +STRING_SUPPORT_CONTACT_DETAIL='Get help with app or store issues' +STRING_SUPPORT_APPLICATION_LOG='Application log' +STRING_SUPPORT_APPLICATION_LOG_DETAIL='Advanced tool to review the app status' +STRING_SUPPORT_SYSTEM_STATUS_REPORT='System status report' +STRING_SUPPORT_SYSTEM_STATUS_REPORT_CLIPBOARD_LABEL='WooCommerce SSR' +STRING_SUPPORT_SYSTEM_STATUS_REPORT_DETAIL='Various system information about your site' +STRING_SUPPORT_SYSTEM_STATUS_REPORT_COPY_LABEL='Copy system status report to clipboard' +STRING_SUPPORT_SYSTEM_STATUS_REPORT_SHARE_LABEL='Share system status report' +STRING_SUPPORT_SYSTEM_STATUS_REPORT_COPIED_TO_CLIPBOARD='System status report copied to clipboard' +STRING_SUPPORT_SYSTEM_STATUS_REPORT_ERROR_COPY_TO_CLIPBOARD='Error copying SSR to clipboard' +STRING_SUPPORT_MOBILE_STATUS_REPORT='Mobile status report' +STRING_SUPPORT_MOBILE_STATUS_REPORT_DETAIL='App, device and store information from this app' +STRING_SUPPORT_MOBILE_STATUS_REPORT_CLIPBOARD_LABEL='WooCommerce mobile status report' +STRING_SUPPORT_MOBILE_STATUS_REPORT_COPY_LABEL='Copy mobile status report to clipboard' +STRING_SUPPORT_MOBILE_STATUS_REPORT_SHARE_LABEL='Share mobile status report' +STRING_SUPPORT_MOBILE_STATUS_REPORT_COPIED_TO_CLIPBOARD='Mobile status report copied to clipboard' +STRING_SUPPORT_MOBILE_STATUS_REPORT_ERROR_COPY_TO_CLIPBOARD='Error copying mobile status report to clipboard' +STRING_SUPPORT_MOBILE_STATUS_REPORT_SHARE_ERROR='Unable to share mobile status report' +STRING_AI_SUPPORT_CHAT_HELP_ROW_TITLE='Chat with support' +STRING_AI_SUPPORT_CHAT_HELP_ROW_SUBTITLE='Chat with our AI assistant to diagnose store issues' +STRING_AI_SUPPORT_CHAT_SCREEN_TITLE='Contact Support' +STRING_AI_SUPPORT_CHAT_STORE_CONNECTION_ERROR_MESSAGE='My store can'"'"'t be reached. The app detected a connection error when trying to reach my store. How can I fix this?' +STRING_AI_SUPPORT_CHAT_HISTORY_ROW_TITLE='Chat history' +STRING_AI_SUPPORT_CHAT_HISTORY_ROW_SUBTITLE='Revisit past support conversations' +STRING_AI_SUPPORT_CHAT_HISTORY_TITLE='Chat history' +STRING_AI_SUPPORT_CHAT_HISTORY_EMPTY='You haven'"'"'t chatted with support yet.' +STRING_AI_SUPPORT_CHAT_HISTORY_LOAD_ERROR='Unable to load chat history.' +STRING_AI_SUPPORT_CHAT_HISTORY_DELETE_ERROR='Unable to delete chat history.' +STRING_AI_SUPPORT_CHAT_HISTORY_DEFAULT_TITLE='Support conversation' +STRING_AI_SUPPORT_CHAT_PLACEHOLDER='Coming soon' +STRING_AI_SUPPORT_CHAT_GREETING='Hello! I'"'"'m your Woo Mobile Support Bot. Is there anything I can help you with today?' +STRING_AI_SUPPORT_CHAT_INPUT_HINT='Type a message…' +STRING_AI_SUPPORT_CHAT_SEND='Send' +STRING_AI_SUPPORT_CHAT_ERROR_TITLE='Error' +STRING_AI_SUPPORT_CHAT_ERROR_DISMISS='Dismiss' +STRING_AI_SUPPORT_CHAT_SEND_ERROR='Unable to send your message. Check your connection and try again.' +STRING_AI_SUPPORT_CHAT_LOAD_HISTORY_ERROR='Unable to load this chat. Check your connection and try again.' +STRING_AI_SUPPORT_CHAT_SUGGESTED_FIX_ERROR='Unable to apply this fix. Check your connection and try again.' +STRING_AI_SUPPORT_CHAT_TYPING='Thinking…' +STRING_AI_SUPPORT_CHAT_HUMAN_SUPPORT_MESSAGE='It looks like you might need additional help. Would you like to contact our support team?' +STRING_AI_SUPPORT_CHAT_CONTACT_SUPPORT='Ask a Happiness Engineer' +STRING_AI_SUPPORT_CHAT_TICKET_CREATED_MESSAGE='A support ticket has been created for this chat. We'"'"'ll respond via email.' +STRING_AI_SUPPORT_CHAT_RESOLVED_MESSAGE='This chat has been marked as resolved.' +STRING_AI_SUPPORT_CHAT_TOOLBAR_CONTACT_SUPPORT='Ask a Happiness Engineer' +STRING_AI_SUPPORT_CHAT_MARK_RESOLVED='Mark Resolved' +STRING_AI_SUPPORT_CHAT_MARK_RESOLVED_CANCEL='Cancel' +STRING_AI_SUPPORT_CHAT_MARK_RESOLVED_CONFIRMATION_TITLE='Mark chat as resolved?' +STRING_AI_SUPPORT_CHAT_MARK_RESOLVED_CONFIRMATION_MESSAGE='This will close the chat actions for this conversation.' +STRING_AI_SUPPORT_CHAT_ESCALATION_CONSENT_TITLE='Send this chat to support?' +STRING_AI_SUPPORT_CHAT_ESCALATION_CONSENT_MESSAGE='We can create a support request using this chat transcript, or you can open the contact form and enter the details yourself.' +STRING_AI_SUPPORT_CHAT_ESCALATION_CONSENT_SEND_REQUEST='Send Request' +STRING_AI_SUPPORT_CHAT_ESCALATION_CONSENT_CONTACT_FORM='Contact Form' +STRING_AI_SUPPORT_CHAT_ESCALATION_TRANSCRIPT_HEADER='Following is the transcript of an in-app AI support chat session:' +STRING_AI_SUPPORT_CHAT_SUPPORT_REQUEST_SUBJECT_MOBILE_APP='Mobile App Support Request' +STRING_AI_SUPPORT_CHAT_SUPPORT_REQUEST_SUBJECT_CARD_READER='Card Reader Support Request' +STRING_AI_SUPPORT_CHAT_SUPPORT_REQUEST_SUBJECT_WOO_PAYMENTS='WooPayments Support Request' +STRING_AI_SUPPORT_CHAT_SUPPORT_REQUEST_SUBJECT_WOO_PLUGIN='WooCommerce Plugin Support Request' +STRING_AI_SUPPORT_CHAT_SUPPORT_REQUEST_SUBJECT_OTHER_PLUGIN='Plugin Support Request' +STRING_AI_SUPPORT_CHAT_POST_DIAGNOSTICS_GREETING='Please describe your issue in more detail so I can help.' +STRING_AI_SUPPORT_CHAT_RESOLVED_PROMPT='Please mark the chat as resolved if your problem is resolved, or leave a message if you have other questions.' +STRING_AI_SUPPORT_CHAT_ISSUE_PICKER_TITLE='What would you like to troubleshoot?' +STRING_AI_SUPPORT_CHAT_ISSUE_LOADING_ORDERS='I can'"'"'t see my orders' +STRING_AI_SUPPORT_CHAT_ISSUE_LOADING_PRODUCTS='I can'"'"'t see my products' +STRING_AI_SUPPORT_CHAT_ISSUE_LOADING_ANALYTICS='My analytics aren'"'"'t loading' +STRING_AI_SUPPORT_CHAT_ISSUE_RECEIVING_NOTIFICATIONS='I'"'"'m not receiving notifications' +STRING_AI_SUPPORT_CHAT_ISSUE_OTHER='Something else' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_SUCCESS='All checks completed with no issues' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_ENABLE_ANALYTICS='Enable Analytics' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_OPEN_NOTIFICATION_SETTINGS='Open Notification Settings' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_REGISTER_PUSH_NOTIFICATIONS='Register Notifications' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_ENABLING_ANALYTICS='Enabling Analytics…' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_REGISTERING_PUSH_NOTIFICATIONS='Registering notifications…' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_RERUN_CHECKS='Run Checks Again' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_CONTINUE='Continue to Chat' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_STATUS_PENDING='Pending' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_STATUS_RUNNING='Running' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_STATUS_PASSED='Passed' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_STATUS_FAILED='Failed' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_ANALYTICS_SETTING_TITLE='Checking analytics setting' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_NOTIFICATION_PERMISSION_TITLE='Checking notification permission' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_APP_NOTIFICATIONS_ENABLED_TITLE='Checking app notification settings' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_NOTIFICATION_CHANNELS_ENABLED_TITLE='Checking notification channels' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_PUSH_TOKEN_TITLE='Checking push notification token' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_PUSH_REGISTRATION_TITLE='Checking push registration' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_WPCOM_CONNECTION_FAILURE='We can'"'"'t connect to WordPress.com right now.\n\nTry again in a few minutes.' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_PARSE_FAILURE='We can'"'"'t work properly with your site'"'"'s response.' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_JETPACK_FAILURE='There is a problem with your Jetpack connection.' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_ANALYTICS_DISABLED_FAILURE='WooCommerce Analytics is not enabled on your store.\n\nAnalytics data like revenue and order stats won'"'"'t be available until it'"'"'s enabled.' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_ANALYTICS_CHECK_FAILURE='We couldn'"'"'t check your analytics setting.' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_NOTIFICATION_SETTINGS_FAILURE='Notifications are blocked in Android settings.\n\nEnable notifications for WooCommerce, then return here to continue.' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_PUSH_TOKEN_FAILURE='We couldn'"'"'t find a push notification token for this device.' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_PUSH_REGISTRATION_FAILURE='This device is not registered to receive push notifications for your store.' +STRING_AI_SUPPORT_CHAT_DIAGNOSTICS_PUSH_CHECK_FAILURE='We couldn'"'"'t check your push notification registration.' +STRING_AI_SUPPORT_CHAT_CONNECTIVITY_INITIAL_MESSAGE='Help me troubleshoot my store connection.' +STRING_AI_SUPPORT_CHAT_CONNECTIVITY_ACTION='Contact Support' +STRING_AI_SUPPORT_CHAT_FEEDBACK_HELPFUL='Helpful' +STRING_AI_SUPPORT_CHAT_FEEDBACK_NOT_HELPFUL='Not helpful' +STRING_SUPPORT_SYSTEM_STATUS_REPORT_SHARE_ERROR='Unable to share System Status Report' +STRING_SUPPORT_SYSTEM_STATUS_REPORT_FETCH_ERROR='Error fetching SSR. Please check WooCommerce -> Status in wp-admin.' +STRING_HELP='Help' +STRING_SUPPORT_CONTACT_EMAIL='Contact email' +STRING_SUPPORT_CONTACT_EMAIL_NOT_SET='Not set' +STRING_SUPPORT_PUSH_NOTIFICATION_TITLE='WooCommerce' +STRING_SUPPORT_PUSH_NOTIFICATION_MESSAGE='New message from '"'"'Help & Support'"'"'' +STRING_LOGVIEWER_ACTIVITY_TITLE='Application log' +STRING_LOGVIEWER_COPIED_TO_CLIPBOARD='Log copied to clipboard' +STRING_LOGVIEWER_ERROR_COPY_TO_CLIPBOARD='Error copying to clipboard' +STRING_LOGVIEWER_SHARE_ERROR='Unable to share log' +STRING_LOGVIEWER_LOG_FILES_LIST_HEADER='Log files by created date' +STRING_LOGVIEWER_LOG_FILES_LIST_FOOTER='Up to seven day'"'"'s worth of logs are stored.' +STRING_LOGVIEWER_CURRENT_LOG_FILE='Current' +STRING_LOGVIEWER_SHARE_ALL_LOGS='Share all logs' +STRING_LOGVIEWER_PREPARING_LOGS='Preparing logs…' +STRING_LOGVIEWER_SHARE_ALL_LOGS_ERROR='Unable to prepare the logs for sharing' +STRING_VERIFICATION_CODE='Verification code' +STRING_INVALID_VERIFICATION_CODE='Invalid verification code' +STRING_ENTER_YOUR_PASSWORD_INSTEAD='Enter your password instead' +STRING_PASSWORD='Password' +STRING_LOG_IN='Log In' +STRING_NEXT='Next' +STRING_OPEN_MAIL='Open Mail' +STRING_ENTER_VERIFICATION_CODE='Enter a verification code to continue.' +STRING_ENTER_VERIFICATION_CODE_SMS_RE='We\ sent\ a\ text\ message\ to\ the\ phone\ number\ ending\ in\ .*\.\ Please\ enter\ the\ verification\ code\ in\ the\ SMS\.' +STRING_ENTER_VERIFICATION_CODE_SMS_GENERIC='We sent a text message. Enter the verification code from the message.' +STRING_LOGIN_USE_WPCOM_USERNAME_INSTEAD='Use username and password instead' +STRING_REQUESTING_OTP='Requesting a verification code via SMS.' +STRING_REQUESTING_SMS_OTP_SUCCESS='SMS requested, please check your messages for the code.' +STRING_REQUESTING_SMS_OTP_FAILURE='SMS request failed. Please try again.' +STRING_PASSWORD_INCORRECT='It looks like this password is incorrect. Please double check your information and try again.' +STRING_OTP_INCORRECT='The OTP code is incorrect. Please double check your information and try again.' +STRING_LOGIN_MAGIC_LINK_EMAIL_REQUESTING='Requesting log-in email' +STRING_LOGIN_MAGIC_LINK_TOKEN_UPDATING='Connecting to your site…' +STRING_MAGIC_LINK_UNAVAILABLE_ERROR_MESSAGE='Currently unavailable. Please enter your password' +STRING_MAGIC_LINK_UPDATE_ERROR='An error has occurred. Please login to continue' +STRING_MAGIC_LINK_FETCH_ACCOUNT_ERROR='There was some trouble fetching your account. You can retry now or close and try again later.' +STRING_ENTER_WPCOM_PASSWORD='Enter your WordPress.com password.' +STRING_ENTER_WPCOM_PASSWORD_GOOGLE='To proceed with this Google account, please provide the matching WordPress.com password. This will be asked only once.' +STRING_LOGIN_CONTINUE='Continue' +STRING_ENTER_SITE_ADDRESS_SHARE_INTENT='Enter the address of your WordPress site you'"'"'d like to share the content to.' +STRING_LOGIN_SITE_ADDRESS_MORE_HELP='Need more help?' +STRING_LOGIN_CHECKING_SITE_ADDRESS='Checking site address' +STRING_LOGIN_ERROR_WHILE_ADDING_SITE_RE='Error\ while\ adding\ site\.\ Error\ code:\ .*' +STRING_LOGIN_LOG_IN_FOR_DEEPLINK='Log in to WordPress.com to access the post.' +STRING_LOGIN_LOG_IN_FOR_SHARE_INTENT='Log in to WordPress.com to share the content.' +STRING_LOGIN_EMPTY_USERNAME='Please enter a username' +STRING_LOGIN_EMPTY_PASSWORD='Please enter a password' +STRING_LOGIN_EMPTY_2FA='Please enter a verification code' +STRING_LOGIN_EMAIL_CLIENT_NOT_FOUND='Can'"'"'t detect your email client app' +STRING_LOGIN_ERROR_BUTTON='Close' +STRING_LOGIN_ERROR_EMAIL_NOT_FOUND_V2='There'"'"'s no WordPress.com account matching this Google account.' +STRING_LOGIN_ERROR_GENERIC='There was some trouble connecting with the Google account.' +STRING_LOGIN_ERROR_SMS_THROTTLED='We'"'"'ve made too many attempts to send an SMS verification code — take a break, and request a new one in a minute.' +STRING_LOGIN_ERROR_GENERIC_START='Google login could not be started.' +STRING_LOGIN_ERROR_SUFFIX='\nMaybe try a different account?' +STRING_SIGNUP_MAGIC_LINK_ERROR='There was some trouble sending the email. You can retry now or close and try again later.' +STRING_SIGNUP_MAGIC_LINK_ERROR_BUTTON_NEGATIVE='Close' +STRING_SIGNUP_MAGIC_LINK_ERROR_BUTTON_POSITIVE='Retry' +STRING_SIGNUP_MAGIC_LINK_PROGRESS='Sending email' +STRING_SIGNUP_WITH_GOOGLE_PROGRESS='Signing up with Google…' +STRING_ENTER_EMAIL_FOR_SITE_RE='Log\ in\ with\ WordPress\.com\ to\ connect\ to\ .*' +STRING_USERNAME='Username' +STRING_ENTER_CREDENTIALS_FOR_SITE_RE='Log\ in\ with\ your\ .*\ site\ credentials' +STRING_LOGIN_SITE_CREDENTIALS_MAGIC_LINK_LABEL_RE='Almost\ there!\ We\ just\ need\ to\ verify\ your\ Jetpack\ connected\ email\ address\ .*' +STRING_LOGIN_DISCOVERY_ERROR_XMLRPC='We were unable to access the XMLRPC file on your site. You will need to reach out to your host to resolve this.' +STRING_LOGIN_DISCOVERY_ERROR_HTTP_AUTH='We were unable to access your site because it requires HTTP Authentication. You will need to reach out to your host to resolve this.' +STRING_LOGIN_DISCOVERY_ERROR_SSL='We were unable to access your site because of a problem with the SSL Certificate. You will need to reach out to your host to resolve this.' +STRING_LOGIN_DISCOVERY_ERROR_GENERIC='We were unable to access your site. You will need to reach out to your host to resolve this.' +STRING_LOGIN_ERROR_XML_RPC_SERVICES_DISABLED='XML-RPC services are disabled on this site.' +STRING_LOGIN_ERROR_XML_RPC_CANNOT_READ_SITE='Unable to read the WordPress site at that URL. Tap on help icon to view the FAQ.' +STRING_LOGIN_ERROR_XML_RPC_CANNOT_READ_SITE_AUTH_REQUIRED='XML-RPC calls seem blocked on this site (error code 401). If attempt to login fails tap on help icon to view the FAQ.' +STRING_LOGIN_ERROR_XML_RPC_AUTH_ERROR_COMMUNICATING='There was a problem communicating with the site. An HTTP error code 401 was returned.' +STRING_GOOGLE_ERROR_TIMEOUT='Google took too long to respond. You may need to wait until you have a stronger internet connection.' +STRING_LOGIN_INVALID_CREDENTIALS_MESSAGE='It seems the username or password you entered doesn'"'"'t quite match. Double-check your credentials and try again.' +STRING_USERNAME_OR_PASSWORD_INCORRECT='The username or password you entered is incorrect' +STRING_CANNOT_ADD_DUPLICATE_SITE='This site already exists in the app, you can'"'"'t add it.' +STRING_DUPLICATE_SITE_DETECTED='A duplicate site has been detected.' +STRING_ERROR_WORDPRESS_COM_CONNECTIVITY='We'"'"'re having trouble reaching WordPress.com. Please check your internet connection settings or try switching networks.' +STRING_ERROR_FETCH_MY_PROFILE='Couldn'"'"'t retrieve your profile' +STRING_ERROR_DISABLED_APIS='Could not fetch settings: Some APIs are unavailable for this OAuth app ID + account combination.' +STRING_LOGIN_TO_TO_CONNECT_JETPACK='Log in to the WordPress.com account you used to connect Jetpack.' +STRING_AUTH_REQUIRED='Log in again to continue.' +STRING_CHECKING_EMAIL='Checking email' +STRING_EMAIL_INVALID='Enter a valid email address' +STRING_NO_SITE_ERROR='The site at this address is not a WordPress site. For us to connect to it, the site must use WordPress.' +STRING_INVALID_SITE_URL_MESSAGE='Check that the site URL entered is valid' +STRING_XMLRPC_MISSING_METHOD_ERROR='Couldn'"'"'t connect. Required XML-RPC methods are missing on the server.' +STRING_XMLRPC_POST_BLOCKED_ERROR='Couldn'"'"'t connect. Your host is blocking POST requests, and the app needs\n that in order to communicate with your site. Contact your host to solve this problem.' +STRING_XMLRPC_ENDPOINT_FORBIDDEN_ERROR='Couldn'"'"'t connect. We received a 403 error when trying to access your\n site XMLRPC endpoint. The app needs that in order to communicate with your site. Contact your host to solve\n this problem.' +STRING_ENTER_WPCOM_OR_JETPACK_SITE='Please enter a WordPress.com or Jetpack-connected self-hosted WordPress site' +STRING_ERROR_GENERIC_NETWORK='A network error occurred. Please check your connection and try again.' +STRING_NOTIFICATION_LOGIN_TITLE_SUCCESS='Logged in!' +STRING_NOTIFICATION_LOGGED_IN='Tap to continue.' +STRING_NOTIFICATION_LOGIN_TITLE_IN_PROGRESS='Login in progress…' +STRING_NOTIFICATION_LOGGING_IN='Please wait while logging in.' +STRING_NOTIFICATION_LOGIN_TITLE_STOPPED='Login stopped' +STRING_NOTIFICATION_ERROR_WRONG_PASSWORD='Please double check your password to continue.' +STRING_NOTIFICATION_2FA_NEEDED='Please provide an authentication code to continue.' +STRING_NOTIFICATION_LOGIN_FAILED='An error has occurred.' +STRING_EMAIL_OR_USERNAME='Email address or Username' +STRING_EMAIL_ADDRESS_LOGIN_TITLE='Email address login' +STRING_SITE_ADDRESS_LOGIN_TITLE='Site address login' +STRING_MAGIC_LINK_LOGIN_TITLE='Magic link login' +STRING_MAGIC_LINK_SENT_LOGIN_TITLE='Magic link sent' +STRING_SELFHOSTED_SITE_LOGIN_TITLE='Login credentials' +STRING_VERIFICATION_2FA_SCREEN_TITLE='Code verification' +STRING_SITE_PICKER_TITLE='Select store' +STRING_SITE_PICKER_ERROR='Error fetching stores' +STRING_SIGNUP_MAGIC_LINK_TITLE='Magic link sent' +STRING_HTTP_AUTHORIZATION_REQUIRED='Authorization required' +STRING_HTTPUSER='HTTP username' +STRING_HTTPPASSWORD='HTTP password' +STRING_APP_RATING_TITLE='Enjoying Woo?' +STRING_APP_RATING_MESSAGE='Nice to see you again! If you’re digging the app, we\’d love a rating on the Google Play Store.' +STRING_APP_RATING_RATE_NOW='Rate now' +STRING_APP_RATING_RATE_LATER='Later' +STRING_APP_RATING_RATE_NEVER='No thanks' +STRING_PROMO_LINKED_PRODUCTS_BANNER_TITLE='Boost your sales with linked products' +STRING_PROMO_LINKED_PRODUCTS_BANNER_MESSAGE='Give your customers helpful and relevant product recommendations by adding upsells and cross-sells' +STRING_PRODUCT_STATUS_PUBLISHED='Published' +STRING_PRODUCT_STATUS_PRIVATELY_PUBLISHED='Privately published' +STRING_PRODUCT_STATUS_PRIVATE='Private' +STRING_PRODUCT_STATUS_PENDING='Pending review' +STRING_PRODUCT_STATUS_DRAFT='Draft' +STRING_PRODUCT_TYPE_SIMPLE='Simple' +STRING_PRODUCT_TYPE_GROUPED='Grouped' +STRING_PRODUCT_TYPE_EXTERNAL='External' +STRING_PRODUCT_TYPE_VARIABLE='Variable' +STRING_PRODUCT_TYPE_PHYSICAL='Physical' +STRING_PRODUCT_TYPE_VIRTUAL='Virtual' +STRING_PRODUCT_TYPE_DOWNLOADABLE='Downloadable' +STRING_PRODUCT_TYPE_SUBSCRIPTION='Subscription' +STRING_PRODUCT_TYPE_VARIABLE_SUBSCRIPTION='Variable subscription' +STRING_PRODUCT_TYPE_BUNDLE='Bundle' +STRING_PRODUCT_TYPE_COMPOSITE='Composite product' +STRING_PRODUCT_TYPE_VARIATION='Variation product' +STRING_PRODUCT_TYPE_LIST_HEADER='Select a product type' +STRING_PRODUCT_TYPE_SIMPLE_TITLE='Simple physical product' +STRING_PRODUCT_TYPE_SIMPLE_DESC='A unique physical product that you may have to ship to the customer' +STRING_PRODUCT_TYPE_SIMPLE_SUBSCRIPTION_TITLE='Simple subscription product' +STRING_PRODUCT_TYPE_SIMPLE_SUBSCRIPTION_DESC='A unique product subscription that enables recurring payments' +STRING_PRODUCT_TYPE_VIRTUAL_TITLE='Simple virtual product' +STRING_PRODUCT_TYPE_VIRTUAL_DESC='A unique digital product like services, downloadable books, music or videos' +STRING_PRODUCT_TYPE_VARIABLE_TITLE='Variable product' +STRING_PRODUCT_TYPE_VARIABLE_DESC='A product with variations like color or size' +STRING_PRODUCT_TYPE_VARIABLE_SUBSCRIPTION_TITLE='Variable subscription product' +STRING_PRODUCT_TYPE_VARIABLE_SUBSCRIPTION_DESC='A product subscription with variations' +STRING_PRODUCT_TYPE_GROUPED_TITLE='Grouped product' +STRING_PRODUCT_TYPE_GROUPED_DESC='A collection of related products' +STRING_PRODUCT_TYPE_EXTERNAL_TITLE='External product' +STRING_PRODUCT_TYPE_EXTERNAL_DESC='Link a product to an external website' +STRING_PRODUCT_TYPE_CONFIRM_DIALOG_TITLE='Are you sure you want to change the product type?' +STRING_PRODUCT_TYPE_CONFIRM_DIALOG_MESSAGE='Changing the product type will modify some of the product data' +STRING_PRODUCT_TYPE_CONFIRM_BUTTON='Yes, change' +STRING_PRODUCT_ADD_TOOL_BAR_TITLE='New Product' +STRING_PRODUCT_ADD_TOOL_BAR_MENU_BUTTON_DONE='Publish' +STRING_PRODUCT_DETAIL_PUBLISH_PRODUCT_ERROR='Error publishing product' +STRING_PRODUCT_DETAIL_SAVE_PRODUCT_ERROR='Cannot update product' +STRING_PRODUCT_DETAIL_PUBLISH_PRODUCT_SUCCESS='Product published' +STRING_PRODUCT_DETAIL_PUBLISH_PRODUCT_DRAFT_ERROR='Error saving product draft' +STRING_PRODUCT_DETAIL_PUBLISH_PRODUCT_DRAFT_SUCCESS='Product draft saved' +STRING_FEEDBACK_REQUEST_TITLE='Enjoying the WooCommerce app?' +STRING_FEEDBACK_REQUEST_MAKE_BETTER='Could be better' +STRING_FEEDBACK_REQUEST_LIKE_IT='I like it' +STRING_FEEDBACK_SURVEY_REQUEST_TITLE='How can we improve?' +STRING_WEB_VIEW_LOADING_TITLE='Loading' +STRING_WEB_VIEW_LOADING_MESSAGE='Please wait…' +STRING_FEEDBACK_COMPLETED_TITLE='Feedback sent' +STRING_FEEDBACK_COMPLETED_TITLE_MESSAGE='Thank you for sharing your\n thoughts with us' +STRING_FEEDBACK_COMPLETED_DESCRIPTION_RE='Keep\ in\ mind\ that\ this\ is\ not\ a\ support\ ticket\ and\ we\ won'"'"'t\ be\ able\ to\ address\ individual\ feedback\.\\n\\nNeed\ some\ help\?\ .*' +STRING_FEEDBACK_COMPLETED_CONTACT_US='Contact us here' +STRING_FEEDBACK_COMPLETED_BACK_TO_STORE_BUTTON_TEXT='Back to store' +STRING_FEEDBACK_PRODUCT_GIVE_FEEDBACK_BUTTON_TEXT='Give Feedback' +STRING_FEEDBACK_BANNER_IPP_TITLE_BEGINNER='Enjoy your in-person payment?' +STRING_FEEDBACK_BANNER_IPP_MESSAGE_BEGINNER='Rate your first in-person payment experience.' +STRING_FEEDBACK_BANNER_IPP_CTA_BUTTON='Share feedback' +STRING_NOTIFICATIONS_PERMISSION_TITLE='Notifications' +STRING_NOTIFICATIONS_PERMISSION_DESCRIPTION='We need your permission to send you push notifications for new orders, reviews, etc. delivered to your device.' +STRING_JETPACK_BENEFITS_BOTTOM_BANNER_TITLE='Get order notifications and more' +STRING_JETPACK_BENEFITS_BOTTOM_BANNER_SUBTITLE='Stay updated and boost store security. Explore Jetpack now.' +STRING_JETPACK_BENEFITS_MODAL_TITLE='Get the most out of your store' +STRING_JETPACK_BENEFITS_MODAL_SUBTITLE='Install the free Jetpack plugin to experience the best mobile experience.' +STRING_JETPACK_BENEFITS_MODAL_PUSH_NOTIFICATIONS_TITLE='Push Notifications' +STRING_JETPACK_BENEFITS_MODAL_PUSH_NOTIFICATIONS_SUBTITLE='Get push notifications for new orders, reviews, etc. delivered to your device.' +STRING_JETPACK_BENEFITS_MODAL_ANALYTICS_TITLE='Analytics' +STRING_JETPACK_BENEFITS_MODAL_ANALYTICS_SUBTITLE='New analytics views, let you see visitors, reports and more.' +STRING_JETPACK_BENEFITS_MODAL_USER_PROFILES_TITLE='User Profiles' +STRING_JETPACK_BENEFITS_MODAL_USER_PROFILES_SUBTITLE='Allow multiple users to access WooCommerce Mobile.' +STRING_JETPACK_BENEFITS_MODAL_INSTALL_JETPACK='Install Jetpack' +STRING_JETPACK_BENEFITS_MODAL_DISMISS='Not Now' +STRING_JETPACK_BENEFITS_FETCHING_STATUS='Fetching Jetpack Status' +STRING_JETPACK_BENEFITS_MODAL_MULTIPLE_STORES_TITLE='Multiple stores' +STRING_JETPACK_BENEFITS_MODAL_MULTIPLE_STORES_SUBTITLE='Get access to all of your WooCommerce stores.' +STRING_JETPACK_BENEFITS_MODAL_LOGIN='Log In to Continue' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_TITLE='Unlock push notifications with WordPress.com' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_BODY='Connect your store to WordPress.com to get access to push notifications for new orders, reviews and more.' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_BODY2='It only takes a minute.' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_WHAT_IS_WPCOM='What is WordPress.com?' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_CONTINUE='Continue' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_NOT_NOW='Not now' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_ERROR_TITLE='Something went wrong' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_ERROR_BODY='We could not complete the push notifications setup. Please contact support for assistance.' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_ERROR_FORBIDDEN_BODY='Your account does not have permission to complete push notifications setup. Please ask your store administrator to handle this.' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_CONNECTED_TITLE='Get push notifications for your store' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_CONNECTED_BODY='You'"'"'re one step away from getting notifications for new orders, reviews and more.' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_UPDATE_REQUIRED_TITLE='Get push notifications for your store' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_UPDATE_REQUIRED_BODY='Your store is already connected to WordPress.com, but you'"'"'ll need to update the WooCommerce plugin to enable push notifications for new orders, reviews, and more.' +STRING_WOO_PUSH_NOTIFICATIONS_INTRODUCTION_UPDATE_PLUGIN='Update plugin' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_TITLE_CONNECT='Connect to WordPress.com' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_BODY_CONNECT_RE='Please\ wait\ while\ we\ finalize\ connecting\ your\ store\ .*\ to\ WordPress\.com\.' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_TITLE_SETUP='Set up push notifications' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_BODY_SETUP_RE='Please\ wait\ while\ we\ set\ up\ push\ notifications\ for\ .*\.' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_STEP_CONNECT_STORE='Connect store to WordPress.com' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_STEP_CHECK_PLUGIN_COMPATIBILITY='Check plugin compatibility' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_STEP_ENABLE_PUSH_NOTIFICATIONS='Enable push notifications' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_COMPLETE='Complete' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_IN_PROGRESS='In progress' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_NOT_STARTED='Not started' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_ERROR='Error' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_ERROR_CONNECTION_PERMISSION_MESSAGE='You don’t have permission to connect this store to WordPress.com' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_GO_TO_MY_STORE='Go to My Store' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_RETRY='Try again' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_GENERIC_ERROR='There was an error completing your request.\nPlease try again or contact support if this error continues.' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_ERROR_PLUGIN_UPDATE_REQUIRED_RE='Your\ current\ WooCommerce\ plugin\ version\ .*\ needs\ updating\ to\ fully\ connect\ your\ store\ to\ WordPress\.com\.' +STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_UPDATE_PLUGIN='Update plugin' +STRING_JETPACK_INSTALL_START_TITLE='Install Jetpack' +STRING_JETPACK_INSTALL_START_SUBTITLE_RE='Install\ the\ free\ Jetpack\ plugin\ to\ .*\ to\ experience\ the\ best\ mobile\ experience\.' +STRING_JETPACK_INSTALL_START_DEFAULT_NAME='your site' +STRING_JETPACK_INSTALL_PROGRESS_TITLE='Installing\nJetpack' +STRING_JETPACK_INSTALL_PROGRESS_SUBTITLE_RE='Please\ wait\ while\ we\ connect\ .*\ with\ Jetpack\.' +STRING_JETPACK_INSTALL_PROGRESS_STEP_ONE_MESSAGE='Installing Jetpack' +STRING_JETPACK_INSTALL_PROGRESS_STEP_TWO_MESSAGE='Activating' +STRING_JETPACK_INSTALL_PROGRESS_STEP_THREE_MESSAGE='Connecting your store' +STRING_JETPACK_INSTALL_PROGRESS_STEP_FOUR_MESSAGE='All done' +STRING_JETPACK_INSTALL_PROGRESS_FAILED_TITLE_RE='Sorry,\ something\ went\ wrong\ during\ .*' +STRING_JETPACK_INSTALL_PROGRESS_FAILED_REASON_INSTALLATION='installation' +STRING_JETPACK_INSTALL_PROGRESS_FAILED_REASON_ACTIVATION='activation' +STRING_JETPACK_INSTALL_PROGRESS_FAILED_REASON_CONNECTION='connecting' +STRING_JETPACK_INSTALL_PROGRESS_FAILED_TRY='Please try again.' +STRING_JETPACK_INSTALL_PROGRESS_FAILED_ALTERNATIVE_RE='Alternatively,\ you\ can\ .*\ Jetpack\ in\ WP\ Admin\.' +STRING_JETPACK_INSTALL_PROGRESS_FAILED_ALTERNATIVE_INSTALL='install' +STRING_JETPACK_INSTALL_PROGRESS_FAILED_ALTERNATIVE_ACTIVATE='activate' +STRING_JETPACK_INSTALL_PROGRESS_FAILED_OPTION_WP_ADMIN_RE='.*\ Jetpack\ in\ WP\ Admin' +STRING_JETPACK_INSTALL_PROGRESS_OPTION_WP_ADMIN_INSTALL='Install' +STRING_JETPACK_INSTALL_PROGRESS_OPTION_WP_ADMIN_ACTIVATE='Activate' +STRING_JETPACK_INSTALL_PROGRESS_FAILED_OPTION_CONTACT_SUPPORT='Contact Support' +STRING_JETPACK_INSTALL_ROLE_ELIGIBILITY_ERROR_MESSAGE='It looks like your role doesn'"'"'t allow you to install Jetpack. \nPlease contact your administrator for help.' +STRING_JETPACK_INSTALL_ROLE_ELIGIBILITY_LEARN_MORE='Learn more about roles and permission' +STRING_INBOX_SCREEN_TITLE='Inbox' +STRING_INBOX_NOTE_RECENCY_NOW='Moments ago' +STRING_INBOX_NOTE_RECENCY_MINUTES_RE='.*\ minutes\ ago' +STRING_INBOX_NOTE_RECENCY_ONE_HOUR='An hour ago' +STRING_INBOX_NOTE_RECENCY_HOURS_RE='.*\ hours\ ago' +STRING_INBOX_NOTE_RECENCY_ONE_DAY='A day ago' +STRING_INBOX_NOTE_RECENCY_DAYS_RE='.*\ days\ ago' +STRING_INBOX_NOTE_RECENCY_DATE_TIME_RE='Created\ on\ .*' +STRING_EMPTY_INBOX_TITLE='Congrats, you’ve read everything!' +STRING_EMPTY_INBOX_DESCRIPTION='Come back soon for more tips and insights on growing your store' +STRING_INBOX_NOTE_SURVEY_ACTIONED='Thank you for your feedback!' +STRING_INBOX_NOTE_DISMISS_ALL_NOTES='Dismiss all' +STRING_INBOX_SCREEN_SYNC_ERROR='Error syncing inbox' +STRING_COUPONS='Coupons' +STRING_COUPON_LIST_ADD_COUPON_CONTENT_DESCRIPTION='Add coupon' +STRING_COUPON_LIST_ITEM_LABEL_ACTIVE='Active' +STRING_COUPON_LIST_ITEM_LABEL_EXPIRED='Expired' +STRING_COUPON_LIST_ITEM_LABEL_EVERYTHING='everything' +STRING_COUPON_LIST_ITEM_LABEL_PRODUCTS_AND_CATEGORIES_RE='.*\ and\ .*' +STRING_COUPON_LIST_ITEM_LABEL_INCLUDED_AND_EXCLUDED_RE='.*\ excl\.\ .*' +STRING_COUPON_LIST_EMPTY_HEADING='No coupons found' +STRING_COUPON_LIST_VIEW_COUPON='View coupon summary' +STRING_COUPON_LIST_LOADING_FAILED='Fetching coupons failed' +STRING_COUPON_LIST_SEARCH_FAILED='Searching coupons failed' +STRING_COUPON_TYPE_PICKER_TITLE='Create Coupon' +STRING_COUPON_TYPE_PICKER_PERCENTAGE_CONTENT_DESCRIPTION='Coupon type - percentage discount' +STRING_COUPON_TYPE_PICKER_FIXED_CART_CONTENT_DESCRIPTION='Coupon type - fixed cart' +STRING_COUPON_TYPE_PICKER_FIXED_PRODUCT_CONTENT_DESCRIPTION='Coupon type - fixed product' +STRING_COUPON_TYPE_PICKER_PERCENTAGE_DISCOUNT_TITLE='Percentage Discount' +STRING_COUPON_TYPE_PICKER_FIXED_CART_DISCOUNT_TITLE='Fixed Cart Discount' +STRING_COUPON_TYPE_PICKER_FIXED_PRODUCT_DISCOUNT_TITLE='Fixed Product Discount' +STRING_COUPON_TYPE_PICKER_PERCENTAGE_DISCOUNT_SUBTITLE='Create a percentage discount for selected products' +STRING_COUPON_TYPE_PICKER_FIXED_CART_DISCOUNT_SUBTITLE='Create a fixed total discount for the entire cart' +STRING_COUPON_TYPE_PICKER_FIXED_PRODUCT_DISCOUNT_SUBTITLE='Create a fixed total discount for selected products' +STRING_COUPON_DETAILS_HEADING='Coupon Summary' +STRING_COUPON_DETAILS_MINIMUM_SPEND_RE='Minimum\ spend\ of\ .*' +STRING_COUPON_DETAILS_MAXIMUM_SPEND_RE='Maximum\ spend\ of\ .*' +STRING_COUPON_DETAILS_PERFORMANCE_HEADING='Performance' +STRING_COUPON_DETAILS_PERFORMANCE_DISCOUNTED_ORDER_HEADING='Discounted Orders' +STRING_COUPON_DETAILS_PERFORMANCE_AMOUNT_HEADING='Amount' +STRING_COUPON_DETAILS_MENU_COPY='Copy Coupon Code' +STRING_COUPON_DETAILS_MENU_SHARE='Share Coupon' +STRING_COUPON_DETAILS_DELETE='Delete Coupon' +STRING_COUPON_DETAILS_DELETE_CONFIRMATION='Are you sure you want to delete this coupon?' +STRING_COUPON_DETAILS_DELETE_FAILURE='Deleting coupon failed' +STRING_COUPON_DETAILS_DELETE_SUCCESSFUL='Coupon deleted' +STRING_COUPON_DETAILS_PERFORMANCE_LOADING_FAILURE='Loading coupon performance failed' +STRING_COUPON_TYPE_PERCENT='Percentage Discount' +STRING_COUPON_TYPE_FIXED_CART='Fixed Cart Discount' +STRING_COUPON_TYPE_FIXED_PRODUCT='Fixed Product Discount' +STRING_COUPON_TYPE_CUSTOM_RE='Custom\ Discount\ \(.*\)' +STRING_COUPON_SUMMARY_TEMPLATE_RE='.*\ off\ .*' +STRING_COUPON_DETAILS_EXPIRATION_DATE_RE='Expires\ .*' +STRING_COUPON_SUMMARY_LOADING_FAILURE='Loading coupon summary failed' +STRING_COUPON_DETAILS_COPY_CLIPBOARD_LABEL='Coupon Code' +STRING_COUPON_DETAILS_COPY_SUCCESS='Coupon code copied to clipboard.' +STRING_COUPON_DETAILS_COPY_ERROR='Error copying coupon code to clipboard.' +STRING_COUPON_DETAILS_SHARE_COUPON_ALL_RE='Apply\ .*\ off\ to\ all\ products\ with\ the\ promo\ code\ .*' +STRING_COUPON_DETAILS_SHARE_COUPON_SOME_RE='Apply\ .*\ off\ to\ select\ products\ with\ the\ promo\ code\ .*' +STRING_COUPON_DETAILS_SHARE_COUPON_ERROR='Error sharing coupon code.' +STRING_COUPON_DETAILS_SHARE_FORMATTING_FAILURE='Unable to generate coupon code sharing message' +STRING_COUPONS_LIST_SEARCH_HINT='Search coupons' +STRING_COUPON_DETAILS_USAGE_LIMIT_PER_USER_MULTIPLE_RE='.*\ uses\ per\ user' +STRING_COUPON_DETAILS_USAGE_LIMIT_PER_USER_SINGLE_RE='.*\ use\ per\ user' +STRING_COUPON_DETAILS_USAGE_LIMIT_PER_COUPON_MULTIPLE_RE='Can\ be\ used\ .*\ times' +STRING_COUPON_DETAILS_USAGE_LIMIT_PER_COUPON_SINGLE_RE='Can\ be\ used\ .*\ time' +STRING_COUPON_DETAILS_USAGE_LIMIT_PER_ITEMS_MULTIPLE_RE='Limited\ to\ .*\ items\ in\ cart' +STRING_COUPON_DETAILS_USAGE_LIMIT_PER_ITEMS_SINGLE_RE='Limited\ to\ .*\ item\ in\ cart' +STRING_COUPON_DETAILS_INDIVIDUAL_USE_ONLY='Individual use only' +STRING_COUPON_DETAILS_ALLOWS_FREE_SHIPPING='Allows free shipping' +STRING_COUPON_DETAILS_EXCLUDES_SALE_ITEMS='Excludes sale items' +STRING_COUPON_DETAILS_RESTRICTED_EMAILS_RE='Restricted\ to\ customers\ with\ emails:\ .*' +STRING_COUPON_DETAILS_MENU_EDIT='Edit Coupon' +STRING_COUPON_EDIT_SCREEN_TITLE_DEFAULT='Edit Coupon' +STRING_COUPON_CREATE_SCREEN_TITLE_DEFAULT='Create Coupon' +STRING_COUPON_EDIT_DETAILS_SECTION='Coupon Details' +STRING_COUPON_EDIT_CONDITIONS_SECTION='Apply this coupon to' +STRING_COUPON_EDIT_AMOUNT_HINT_RE='Amount\ \(.*\)' +STRING_COUPON_EDIT_AMOUNT_PERCENTAGE_HELPER='Set the percentage of the discount you want to offer.' +STRING_COUPON_EDIT_AMOUNT_RATE_HELPER='Set the amount of the discount you want to offer.' +STRING_COUPON_EDIT_CODE_HINT='Coupon Code' +STRING_COUPON_EDIT_CODE_HELPER='Customers need to enter this code to use the coupon.' +STRING_COUPON_EDIT_REGENERATE_COUPON='Regenerate Coupon Code' +STRING_COUPON_EDIT_SAVE_BUTTON='Save' +STRING_COUPON_CREATE_SAVE_BUTTON='Create' +STRING_COUPON_EDIT_ADD_DESCRIPTION='Add Description (Optional)' +STRING_COUPON_EDIT_EDIT_DESCRIPTION='Edit Description' +STRING_COUPON_EDIT_DESCRIPTION_EDITOR_TITLE='Coupon Description' +STRING_COUPON_EDIT_ADD_DESCRIPTION_HINT='Add the description of the coupon.' +STRING_COUPON_EDIT_EXPIRY_DATE='Coupon Expiry Date' +STRING_COUPON_EDIT_EXPIRY_DATE_NONE='None' +STRING_COUPON_EDIT_EXPIRY_CLEAR_EXPIRY_DATE='Clear' +STRING_COUPON_EDIT_FREE_SHIPPING='Include Free Shipping?' +STRING_COUPON_EDIT_USAGE_SECTION='Usage Details' +STRING_COUPON_EDIT_USAGE_RESTRICTIONS='Usage Restrictions' +STRING_COUPON_EDIT_SELECT_CATEGORIES_TITLE='Select Product Categories' +STRING_COUPON_EDIT_EDIT_PRODUCTS_TITLE_RE='Edit\ Product\ Categories\ \(.*\)' +STRING_COUPON_EDIT_COUPON_UPDATED='Coupon updated' +STRING_COUPON_CREATE_COUPON_CREATED='Coupon created' +STRING_COUPON_EDIT_COUPON_UPDATE_FAILED='Updating coupon failed' +STRING_COUPON_CREATE_COUPON_CREATION_FAILED='Creating coupon failed' +STRING_COUPON_EDIT_SAVING_DIALOG_TITLE='Saving coupon' +STRING_COUPON_EDIT_SAVING_DIALOG_SUBTITLE='Please wait…' +STRING_COUPON_RESTRICTIONS_MINIMUM_SPEND_HINT_RE='Minimum\ Spend\ \(.*\)' +STRING_COUPON_RESTRICTIONS_MAXIMUM_SPEND_HINT_RE='Maximum\ Spend\ \(.*\)' +STRING_COUPON_RESTRICTIONS_MINIMUM_MAXIMUM_SPEND_PLACEHOLDER='None' +STRING_COUPON_RESTRICTIONS_LIMIT_PER_COUPON_HINT='Usage Limit Per Coupon' +STRING_COUPON_RESTRICTIONS_LIMIT_PER_COUPON_PLACEHOLDER='Unlimited' +STRING_COUPON_RESTRICTIONS_AMOUNT_LIMIT_HINT='Limit Usage To X Items' +STRING_COUPON_RESTRICTIONS_AMOUNT_LIMIT_PLACEHOLDER='All Qualifying' +STRING_COUPON_RESTRICTIONS_LIMIT_PER_USER_HINT='Usage Limit Per User' +STRING_COUPON_RESTRICTIONS_LIMIT_PER_USER_PLACEHOLDER='Unlimited' +STRING_COUPON_RESTRICTIONS_INDIVIDUAL_USE='Individual Use Only' +STRING_COUPON_RESTRICTIONS_INDIVIDUAL_USE_HINT='Turn this on if the coupon cannot be used in conjunction with other coupons.' +STRING_COUPON_RESTRICTIONS_EXCLUDE_SALE_ITEMS='Exclude Sale Items' +STRING_COUPON_RESTRICTIONS_EXCLUDE_SALE_ITEMS_HINT='Turn this on if the coupon should not apply to items on sale. Per-item coupons will only work if the item is not on sale. Per-cart coupons will only work if there are items in the cart that are not on sale.' +STRING_COUPON_RESTRICTIONS_ALLOWED_EMAILS='Allowed emails' +STRING_COUPON_RESTRICTIONS_ALLOWED_EMAILS_PLACEHOLDER='No restrictions' +STRING_COUPON_RESTRICTIONS_ALLOWED_EMAILS_HINT='List of allowed billing emails to check against when an order is placed. Separate email addresses with commas. You can also use an asterisk (*) to match parts of an email. For example "*@gmail.com" would match all gmail addresses.' +STRING_COUPON_RESTRICTIONS_ALLOWED_EMAILS_INVALID='Some email address are invalid. Please fix the entered email(s).' +STRING_COUPON_RESTRICTIONS_EXCLUSIONS_SECTION_TITLE='Apply this coupon to' +STRING_COUPON_RESTRICTIONS_EXCLUDE_PRODUCTS='Exclude Products' +STRING_COUPON_RESTRICTIONS_EXCLUDE_CATEGORIES='Exclude Product Categories' +STRING_COUPON_CONDITIONS_PRODUCTS_SELECT_PRODUCTS_TITLE='Select Products' +STRING_COUPON_CONDITIONS_PRODUCTS_ALL_PRODUCTS_TITLE='All Products' +STRING_COUPON_CONDITIONS_PRODUCTS_EDIT_PRODUCTS_TITLE_RE='Edit\ Products\ \(.*\)' +STRING_PRODUCT_SELECTOR_SELECT_PRODUCT_LABEL_RE='Select\ product\ .*' +STRING_PRODUCT_SELECTOR_SELECT_VARIATION_LABEL_RE='Select\ variation\ .*' +STRING_PRODUCT_SELECTOR_SKU_VALUE_RE='SKU:\ .*' +STRING_PRODUCT_SELECTOR_SELECT_BUTTON_TITLE_DEFAULT_RE='Select\ .*\ Products' +STRING_PRODUCT_SELECTOR_SELECT_BUTTON_TITLE_ONE_RE='Select\ .*\ Product' +STRING_PRODUCT_SELECTOR_CLEAR_BUTTON_TITLE='Clear Selection' +STRING_PRODUCT_SELECTOR_FILTER_BUTTON_TITLE_ZERO='Filter' +STRING_PRODUCT_SELECTOR_FILTER_BUTTON_TITLE_DEFAULT_RE='Filter\ \(.*\)' +STRING_PRODUCT_SELECTOR_ARROW_CONTENT_DESCRIPTION='Show details' +STRING_PRODUCT_SELECTOR_SEARCH_HINT='Search Products' +STRING_PRODUCT_SELECTOR_LOADING_FAILED='Loading products failed' +STRING_PRODUCT_SELECTOR_SEARCH_FAILED='Searching products failed' +STRING_PRODUCT_SELECTOR_EMPTY_STATE='No products found' +STRING_PRODUCT_SELECTOR_CLEAR_FILTERS_BUTTON_TITLE='Clear Filters' +STRING_PRODUCT_SELECTOR_POPULAR_PRODUCTS_HEADING='Popular' +STRING_PRODUCT_SELECTOR_RECENT_PRODUCTS_HEADING='Last Sold' +STRING_PRODUCT_SELECTOR_PRODUCTS_HEADING='Products' +STRING_PRODUCT_SELECTOR_SUBSCRIPTION_NOT_SUPPORTED='Subscription products are not supported for order creation' +STRING_INSTALL_WC_SHIPPING_FLOW_ONBOARDING_SCREEN_TITLE='Fulfill your orders with WooCommerce Shipping' +STRING_INSTALL_WC_SHIPPING_FLOW_ONBOARDING_SCREEN_SUBTITLE='Save time and money' +STRING_INSTALL_WC_SHIPPING_FLOW_ONBOARDING_SCREEN_POSTAGE_BULLET_TITLE='Buy postage when you need it' +STRING_INSTALL_WC_SHIPPING_FLOW_ONBOARDING_SCREEN_POSTAGE_BULLET_DESC='No need to wonder where that stampbook went.' +STRING_INSTALL_WC_SHIPPING_FLOW_ONBOARDING_SCREEN_PRINT_BULLET_TITLE='Print from your phone' +STRING_INSTALL_WC_SHIPPING_FLOW_ONBOARDING_SCREEN_PRINT_BULLET_DESC='Pick up an order, then just pay, print, package, and post.' +STRING_INSTALL_WC_SHIPPING_FLOW_ONBOARDING_SCREEN_DISCOUNTS_BULLET_TITLE='Discounted rates' +STRING_INSTALL_WC_SHIPPING_FLOW_ONBOARDING_SCREEN_DISCOUNTS_BULLET_DESC='Access discounted shipping rates. Currently available with DHL and USPS, with more to come soon!' +STRING_INSTALL_WC_SHIPPING_FLOW_ONBOARDING_SCREEN_LINK='What is WooCommerce Shipping?' +STRING_INSTALL_WC_SHIPPING_FLOW_ONBOARDING_SCREEN_ADD_EXTENSION_BUTTON='Add Extension To Store' +STRING_INSTALL_WC_SHIPPING_FLOW_ONBOARDING_SCREEN_NOT_NOW_BUTTON='Not Now' +STRING_INSTALL_WC_SHIPPING_EXTENSION_NAME='WooCommerce Shipping' +STRING_ERROR_LOADING_IMAGE='Unable to load image' +STRING_LOGIN_INVALID_SITE_URL='Please enter a complete website address, like example.com.' +STRING_NOTIFICATION_WPCOM_USERNAME_NEEDED='Please log in with your username and password.' +STRING_SEND_LINK_BY_EMAIL='Send link by email' +STRING_CREATE_ACCOUNT='Create account' +STRING_OR_TYPE_YOUR_PASSWORD='Or type your password' +STRING_ENTER_EMAIL_TO_CONTINUE_WORDPRESS_COM='Enter your email address to log in or create a WordPress.com account.' +STRING_ENTER_ACCOUNT_INFO_FOR_SITE_RE='Enter\ your\ account\ information\ for\ .*\.' +STRING_GET_STARTED='Get Started' +STRING_SIMPLIFIED_LOGIN_PROLOGUE_INTRO='The ecommerce platform that grows with you' +STRING_SIMPLIFIED_LOGIN_PROLOGUE_TITLE='From your first sale to millions in revenue, Woo is with you. See why merchants trust us to power 3.4 million online stores.' +STRING_CHECK_EMAIL='Check email' +STRING_LOGIN_TEXT_OTP='Resend code via text message' +STRING_LOGIN_TEXT_OTP_ANOTHER='Resend code via text message' +STRING_LOGIN_MAGIC_LINKS_SENT_LABEL='Check your email on this device and tap the link in the email you received from WordPress.com.' +STRING_MAGIC_LINK_NOT_SEEING_EMAIL_MESSAGE='Not seeing the email? Check your Spam or Junk Mail folder.' +STRING_LOGIN_FIND_YOUR_CONNECTED_EMAIL='Find your connected email' +STRING_CONTINUE_GOOGLE_BUTTON_SUFFIX='Continue with Google' +STRING_CONTINUE_SITE_CREDENTIALS='Continue with store credentials' +STRING_LOGIN_OR='or' +STRING_SIGN_UP_LABEL='Sign Up' +STRING_SIGNUP_CONFIRMATION_MESSAGE='We’ll use this email address to create your new WordPress.com account.' +STRING_CONTINUE_TERMS_OF_SERVICE_TEXT_RE='By\ continuing,\ you\ agree\ to\ our\ .*Terms\ of\ Service.*\.' +STRING_CONTINUE_WITH_GOOGLE_TERMS_OF_SERVICE_TEXT_RE='If\ you\ continue\ with\ Google\ and\ don'"'"'t\ already\ have\ a\ WordPress\.com\ account,\ you\ are\ creating\ an\ account\ and\ you\ agree\ to\ our\ .*Terms\ of\ Service.*\.' +STRING_RESET_YOUR_PASSWORD='Reset your password' +STRING_SIGNUP_CONFIRMATION_TITLE='Signup confirmation' +STRING_WHAT_IS_WORDPRESS_LINK='What is WordPress.com?' +STRING_WP_EMAIL_INPUT_FALLBACK_LOG_IN_WITH_SITE_CREDENTIALS='Log in with your site credentials' +STRING_LOGIN_SITE_ADDRESS='Site address' +STRING_LOGIN_MAGIC_LINK_BUTTON='Log in with magic link' +STRING_USERNAME_NOT_REGISTERED_WPCOM='We can'"'"'t find a WordPress.com account connected to this username. You can enter an email to create a new account.' +STRING_LOGIN_MAGIC_LINKS_LABEL='We'"'"'ll email you a link that'"'"'ll log you in instantly, no password needed.' +STRING_LOGIN_GET_LINK_BY_EMAIL='Get a login link by email' +STRING_SIGNUP_MAGIC_LINK_MESSAGE='We’ve emailed you a signup link to create your new WordPress.com account. Check your email on this device, and tap the link in the email you receive from WordPress.com.' +STRING_ENTER_EMAIL_WORDPRESS_COM='Log in with your WordPress.com account email address to manage your WooCommerce stores.' +STRING_LOGIN_SITE_ADDRESS_HELP_TITLE='What'"'"'s my site address?' +STRING_LOGIN_SITE_ADDRESS_HELP_CONTENT='Your site address appears in the bar at the top of the screen when you visit your site in Chrome.' +STRING_LOGIN_FIND_YOUR_SITE_ADRESS='Find your site address' +STRING_PRODUCT_DOWNLOADABLE_FILES_VALUE_MULTIPLE_RE='.*\ files' +STRING_PRODUCT_DOWNLOADABLE_FILES_VALUE_SINGLE='1 file' +STRING_PRODUCT_DOWNLOADABLE_FILES_ADD='Add file' +STRING_PRODUCT_DOWNLOADABLE_FILES_URL='File URL' +STRING_PRODUCT_DOWNLOADABLE_FILES_URL_INFO='This is the URL of the file which customers will get access to. URLs entered should already be encoded.' +STRING_PRODUCT_DOWNLOADABLE_FILES_NAME='File Name' +STRING_PRODUCT_DOWNLOADABLE_FILES_NAME_INFO='This is the name of the file shown to the customer' +STRING_PRODUCT_DOWNLOADABLE_FILES_LIMIT_INFO='Enter the number of time file can be downloaded or leave blank for unlimited downloads' +STRING_PRODUCT_DOWNLOADABLE_FILES_EXPIRY_INFO='Enter the number of days before a download limk expires, or leave blank if never it expires' +STRING_PRODUCT_DOWNLOADABLE_FILES_LIMIT='Download limit' +STRING_PRODUCT_DOWNLOADABLE_FILES_EXPIRY='Download expiration' +STRING_DELETE='Delete' +STRING_PRODUCT_IS_DOWNLOADABLE='Downloadable product' +STRING_PRODUCT_DOWNLOADABLE_FILES_DELETE_CONFIRMATION='Are you sure you want to remove this file?' +STRING_PRODUCT_DOWNLOADABLE_FILES_EDIT_TITLE='File' +STRING_PRODUCT_UNCHECK_IS_DOWNLOADABLE_WARNING_TITLE='Are you sure you want to remove the ability to download files when product is purchased?' +STRING_PRODUCT_UNCHECK_IS_DOWNLOADABLE_WARNING_MESSAGE='All files currently attached to this product will be removed.' +STRING_PRODUCT_UNCHECK_IS_DOWNLOADABLE_WARNING_YES_BUTTON='Yes, change' +STRING_PRODUCT_UNCHECK_IS_DOWNLOADABLE_WARNING_NO_BUTTON='Cancel' +STRING_BOTTOM_SHEET_DOWNLOADABLE_FILES_DESC='Include downloadable files with purchases' +STRING_PRODUCT_DOWNLOADABLE_ADD_BOTTOMSHEET_HEADER='Add downloadable file from' +STRING_PRODUCT_DOWNLOADABLE_FILES_ADD_TITLE='Add downloadable file' +STRING_PRODUCT_DOWNLOADABLE_FILES_UPLOAD_FAILED='Error uploading the file' +STRING_PRODUCT_DOWNLOADABLE_FILES_UPLOAD_DIALOG_TITLE='Uploading files' +STRING_PRODUCT_DOWNLOADABLE_FILES_UPLOAD_DIALOG_MESSAGE='Please wait…' +STRING_PRODUCT_DOWNLOADABLE_FILES_URL_INVALID='Check that the url entered is valid' +STRING_PRODUCT_DOWNLOADABLE_FILES_ADD_MEDIA_FROM_DEVICE='Images and videos on device' +STRING_PRODUCT_DOWNLOADABLE_FILES_ADD_DOCUMENTS_FROM_DEVICE='Documents and other files on device' +STRING_PRODUCT_DOWNLOADABLE_FILES_ADD_FROM_WPMEDIA_LIBRARY='WordPress Media Library' +STRING_PRODUCT_DOWNLOADABLE_FILES_ADD_MANUALLY='Enter file URL' +STRING_PRODUCT_DOWNLOADABLE_FILES_NAME_INVALID='Please enter a valid name' +STRING_PRODUCT_DOWNLOADABLE_FILES_DOWNLOAD_SETTINGS='Download Settings' +STRING_SHIPPING_LABEL_PAYMENTS_CANT_EDIT_WARNING_RE='Only\ the\ site\ owner\ can\ manage\ the\ shipping\ label\ payment\ methods\.\ Please\ contact\ Store\ Owner\ .*\ \(.*\)\ to\ manage\ payment\ methods\.' +STRING_MAGIC_LINK_NOT_MEANING_TO_CREATE_ACCOUNT='Didn'"'"'t mean to create a new account? Go back to re-enter your email address.' +STRING_ABOUT_AUTOMATTIC_WORK_WITH_US_ITEM_TITLE='Work With Us' +STRING_ABOUT_AUTOMATTIC_WORK_WITH_US_ITEM_SUBTITLE='Work from Anywhere' +STRING_ERROR_GENERIC='An error occurred' +STRING_INSTALL_WC_SHIPPING_PREINSTALL_TITLE='Install extension' +STRING_INSTALL_WC_SHIPPING_INSTALLATION_INFO='Things to know before installing' +STRING_INSTALL_WC_SHIPPING_PROCEED_BUTTON='Proceed With Installation' +STRING_TESTS_NOTIFICATION_NEW_ORDER_TITLE='You have a new order! 🎉' +STRING_TESTS_NOTIFICATION_NEW_ORDER_MESSAGE='New order for $50 on Your WooCommerce Store' +STRING_LOGIN_MAGIC_LINKS_SENT_LABEL_SHORT='Check your email on this device!' +STRING_LOGIN_MAGIC_LINKS_EMAIL_SENT='We just sent a magic link to' +STRING_LOGIN_MAGIC_LINKS_EMAIL_SENT_DOUBLE_CHECK_EMAIL='Ensure your email is correct and double check your spam folder.' +STRING_LOGIN_MAGIC_LINKS_EMAIL_SENT_TO_UNKNOWN_EMAIL='We just sent a magic link to your account'"'"'s email address' +STRING_OR_USE_PASSWORD_BELOW_QR_CODE_SCAN_OPTION='Or log in with password' +STRING_STATS_WIDGET_LOG_IN_MESSAGE='Please log in to the WooCommerce app' +STRING_STATS_WIDGET_OFFLINE_ERROR='Your network is unavailable.\nCheck your data or wifi connection.' +STRING_STATS_WIDGET_BATTERY_SAVER_ERROR='Looks like your device is in Battery Saver mode. \nWe can'"'"'t provide your store information while it'"'"'s active' +STRING_STATS_WIDGET_AVAILABILITY_MESSAGE='Store analytics not available! Please upgrade to the latest version of WooCommerce to view your store analytics.' +STRING_STATS_TODAY_WIDGET_DESCRIPTION='WooCommerce Stats Today' +STRING_STATS_WIDGET_ERROR_NO_DATA='Couldn'"'"'t load data' +STRING_STATS_WIDGET_LAST_UPDATED_MESSAGE_RE='As\ of\ .*' +STRING_THEME_PICKER_SETTINGS_TITLE='Try a new look' +STRING_THEME_PICKER_CURRENT_THEME_TITLE='Current theme' +STRING_THEME_PICKER_CAROUSEL_INFO_ITEM_TITLE='Looking for more?' +STRING_THEME_PICKER_CAROUSEL_INFO_ITEM_DESCRIPTION_SETTINGS='You can find your perfect theme in the WooCommerce Theme Store.' +STRING_THEME_PICKER_CAROUSEL_ERROR_PLACEHOLDER_MESSAGE_RE='Sorry,\ it\ seems\ there\ is\ an\ issue\ with\ the\ template\ loading\.\ Please\ .*\ for\ a\ live\ demo\.' +STRING_THEME_PICKER_CAROUSEL_ERROR_PLACEHOLDER_MESSAGE_CTA='tap here' +STRING_THEME_PICKER_ERROR_MESSAGE='Failed to load themes.' +STRING_THEME_PICKER_LOADING_CURRENT_THEME_FAILED='Couldn'"'"'t load your current theme' +STRING_THEME_PREVIEW_TITLE='Preview' +STRING_THEME_PREVIEW_BOTTOM_SHEET_PAGES_TITLE='Pages on this template' +STRING_THEME_PREVIEW_BOTTOM_SHEET_PAGES_SUBTITLE='Tap to view' +STRING_THEME_PREVIEW_BOTTOM_SHEET_HOME_SECTION='Home' +STRING_THEME_ACTIVATED_SUCCESSFULLY='Theme activated successfully' +STRING_THEME_PREVIEW_TYPE_MOBILE='Mobile' +STRING_THEME_PREVIEW_TYPE_TABLET='Tablet' +STRING_THEME_PREVIEW_TYPE_DESKTOP='Desktop' +STRING_THEME_PREVIEW_ACTIVATE_THEME_BUTTON_SETTINGS_RE='Use\ .*' +STRING_THEME_ACTIVATION_FAILED='Theme activation failed, please try again!' +STRING_STORE_ONBOARDING_TITLE='Store setup' +STRING_STORE_ONBOARDING_TASK_ABOUT_YOUR_STORE_TOOLBAR_TITLE='About your store' +STRING_STORE_ONBOARDING_TASK_ABOUT_YOUR_STORE_TITLE='Tell us more about your store' +STRING_STORE_ONBOARDING_TASK_ABOUT_YOUR_STORE_DESCRIPTION='We’ll use the info to get a head start on your shipping, tax, and payments settings.' +STRING_STORE_ONBOARDING_TASK_ADD_PRODUCT_TITLE='Add your first product' +STRING_STORE_ONBOARDING_TASK_ADD_PRODUCT_DESCRIPTION='Start selling by adding products or services to your store.' +STRING_STORE_ONBOARDING_TASK_LAUNCH_STORE_TITLE='Launch your store' +STRING_STORE_ONBOARDING_TASK_LAUNCH_STORE_DESCRIPTION='Publish your site to the world anytime you want!' +STRING_STORE_ONBOARDING_TASK_PAYMENTS_SETUP_TITLE='Get paid' +STRING_STORE_ONBOARDING_TASK_PAYMENTS_SETUP_DESCRIPTION='Give your customers an easy and convenient way to pay!' +STRING_STORE_ONBOARDING_TASK_WOOPAYMENTS_SETUP_DESCRIPTION='Manage payments seamlessly with WooPayments, free from setup or monthly fees.' +STRING_STORE_ONBOARDING_TASK_WOOPAYMENTS_CELEBRATION_HEADER='You did it!' +STRING_STORE_ONBOARDING_TASK_WOOPAYMENTS_CELEBRATION_MESSAGE='Congratulations! You'"'"'ve successfully navigated through the setup and your payment system is ready to roll.' +STRING_STORE_ONBOARDING_TASK_NAME_STORE_TITLE='Name your store' +STRING_STORE_ONBOARDING_TASK_NAME_STORE_DESCRIPTION='Customizing your store name can also help your store'"'"'s search engine optimization.' +STRING_STORE_ONBOARDING_COMPLETED_TASKS_STATUS_RE='.*/.*\ completed' +STRING_STORE_ONBOARDING_COMPLETED_TASKS_FULL_SCREEN_STATUS_RE='.*\ of\ .*\ tasks\ completed' +STRING_STORE_ONBOARDING_TASK_VIEW_ALL_TASKS='View all tasks' +STRING_STORE_ONBOARDING_TASK_VIEW_ALL_RE='View\ all\ \(.*\)' +STRING_STORE_ONBOARDING_TASK_PRODUCT_DESCRIPTION_AI_GENERATOR_TEXT='AI content generator available' +STRING_STORE_ONBOARDING_FULL_SCREEN_TRANSITION_NAME='Onboarding full screen' +STRING_STORE_ONBOARDING_MENU_HIDE_STORE_SETUP='Hide store setup list' +STRING_STORE_ONBOARDING_LAUNCH_STORE_BUTTON='Publish my store' +STRING_STORE_ONBOARDING_LAUNCH_STORE_SHARE_URL_BUTTON='Share URL' +STRING_STORE_ONBOARDING_LAUNCH_STORE_BACK_TO_STORE_BUTTON='Back to My Store' +STRING_STORE_ONBOARDING_LAUNCH_PREVIEW_TITLE='Preview' +STRING_STORE_ONBOARDING_LAUNCHED_TITLE='Your store is live!' +STRING_STORE_ONBOARDING_LAUNCH_STORE_TASK_PRIVATE_TAG='Private' +STRING_STORE_ONBOARDING_SHARE_URL_ERROR='Unable to share store url' +STRING_STORE_ONBOARDING_STORE_ALREADY_LAUNCHED_ERROR_TITLE='Could not launch your store' +STRING_STORE_ONBOARDING_STORE_ALREADY_LAUNCHED_ERROR_DESCRIPTION='We found that the store has already launched.' +STRING_STORE_ONBOARDING_LAUNCH_STORE_GENERIC_ERROR_TITLE='Unexpected error' +STRING_STORE_ONBOARDING_LAUNCH_STORE_GENERIC_ERROR_DESCRIPTION='Oops, some unexpected errors happened.' +STRING_STORE_ONBOARDING_LAUNCH_STORE_OK='OK' +STRING_STORE_ONBOARDING_NAME_YOUR_STORE_DIALOG_TITLE='Update store name' +STRING_STORE_ONBOARDING_NAME_YOUR_STORE_DIALOG_LABEL='Store name' +STRING_STORE_ONBOARDING_NAME_YOUR_STORE_DIALOG_SUCCESS='Store name set! \n To change again, visit Store Settings.' +STRING_SETTINGS_NAME_YOUR_STORE_DIALOG_SUCCESS='Store name set!' +STRING_SETTINGS_NAME_YOUR_STORE_DIALOG_LOADING='Saving new store name…' +STRING_STORE_ONBOARDING_NAME_YOUR_STORE_DIALOG_FAILURE='Unable to save store name. Please try again.' +STRING_STORE_ONBOARDING_WCPAY_INSTRUCTIONS_TITLE='Manage payments effortlessly with WooPayments all in one place. Accept cards, Apple Pay, in-person payments, and 135+ currencies, all with zero setup costs or monthly fees.' +STRING_STORE_ONBOARDING_WCPAY_INSTRUCTIONS_ESTIMATE_TITLE='Estimated setup time' +STRING_STORE_ONBOARDING_WCPAY_INSTRUCTIONS_ESTIMATE_TIME='4–6 minutes' +STRING_STORE_ONBOARDING_WCPAY_INSTRUCTIONS_CONTENT_TITLE='Before your start setup' +STRING_STORE_ONBOARDING_WCPAY_INSTRUCTIONS_CONTENT_STEP_1_CONTENT='Your WooPayments notifications will be sent to your WordPress.com account email. Prefer a new account? More details here.' +STRING_STORE_ONBOARDING_WCPAY_INSTRUCTIONS_CONTENT_STEP_2_CONTENT='We'"'"'ve partnered with Stripe for WooPayments. You'"'"'ll be directed to Stripe'"'"'s site for sign-up. We'"'"'ll ask you to verify your business and payment details.' +STRING_STORE_ONBOARDING_WCPAY_INSTRUCTIONS_CONTENT_BUTTON='Begin setup' +STRING_STORE_ONBOARDING_WCPAY_INSTRUCTIONS_CONTENT_LEARN_MORE='Learn more about verifying your information with WooPayments.' +STRING_STORE_ONBOARDING_PAYMENTS_PRE_SETUP_TITLE_ONE='Set up' +STRING_STORE_ONBOARDING_PAYMENTS_PRE_SETUP_TITLE_WCPAY='WooPayments' +STRING_STORE_ONBOARDING_PAYMENTS_PRE_SETUP_TITLE_GENERIC='Payment Methods' +STRING_STORE_ONBOARDING_WCPAY_SETUP_DESCRIPTION='By using WooCommerce Payments you agree to be bound by our Terms of Service and acknowledge that you have read our Privacy Policy.' +STRING_STORE_ONBOARDING_PAYMENTS_SETUP_DESCRIPTION='Discover other payment providers and \nchoose a payment provider.' +STRING_STORE_ONBOARDING_LAUNCH_STORE_RENDERING_PREVIEW_LABEL='Rendering preview…' +STRING_SUPPORT_REQUEST='Support Request' +STRING_SUPPORT_REQUEST_HELP_TITLE='I need help with' +STRING_SUPPORT_REQUEST_TITLE='Let'"'"'s get this sorted' +STRING_SUPPORT_REQUEST_DESCRIPTION='Let us know your site address (URL) and tell us as much as you can about the problem, and we will be in touch soon.' +STRING_SUPPORT_REQUEST_SUBJECT='Subject' +STRING_SUPPORT_REQUEST_SITE_ADDRESS='Site Address' +STRING_SUPPORT_REQUEST_SUBMIT='Submit Support Request' +STRING_SUPPORT_REQUEST_MESSAGE_HINT='Write something' +STRING_SUPPORT_REQUEST_HELP_APP='Mobile App' +STRING_SUPPORT_REQUEST_HELP_IPP='Card Reader / In-Person Payments' +STRING_SUPPORT_REQUEST_HELP_PAYMENTS='WooCommerce Payments' +STRING_SUPPORT_REQUEST_HELP_PLUGINS='WooCommerce Plugin' +STRING_SUPPORT_REQUEST_HELP_OTHER='Other Extension / Plugin' +STRING_SUPPORT_REQUEST_LOADING_TITLE='Sending your request' +STRING_SUPPORT_REQUEST_LOADING_MESSAGE='Please wait…' +STRING_SUPPORT_REQUEST_SUCCESS_TITLE='Request sent!' +STRING_SUPPORT_REQUEST_SUCCESS_MESSAGE='Your support request has landed safely in our inbox. We will reply via email as quickly as we can.' +STRING_SUPPORT_REQUEST_DIALOG_ACTION='Got It!' +STRING_SUPPORT_REQUEST_ERROR_TITLE='Something went wrong' +STRING_SUPPORT_REQUEST_ERROR_MESSAGE='Sorry, we cannot create support requests right now, please try again later.' +STRING_FREE_TRIAL_YOUR_TRIAL_ENDED='Your trial has ended.' +STRING_FREE_TRIAL_TRIAL_ENDED='Trial ended' +STRING_FREE_TRIAL_DAYS_LEFT_RE='.*\ left\ in\ your\ trial\.' +STRING_FREE_TRIAL_ONE_DAY_LEFT='1 day' +STRING_FREE_TRIAL_DAYS_LEFT_PLURAL_RE='.*\ days' +STRING_UPGRADES_REPORT_SUBSCRIPTION_ISSUE='Report subscription issue' +STRING_UPGRADES_CURRENT_PLAN_RE='Current:\ .*' +STRING_UPGRADES_TROUBLESHOOTING='Troubleshooting' +STRING_UPGRADES_TITLE='Subscriptions' +STRING_UPGRADES_SUBSCRIPTION_STATUS='Subscription status' +STRING_UPGRADES_UPGRADEABLE_CAPTION_RE='You\ are\ in\ the\ .*\-day\ free\ trial\.\ The\ free\ trial\ will\ end\ in\ .*\.\ Upgrade\ to\ unlock\ new\ features\ and\ keep\ your\ store\ running\.' +STRING_UPGRADES_TRIAL_ENDED_CAPTION_RE='Your\ free\ trial\ has\ ended\ and\ have\ limited\ access\ to\ all\ the\ features\.\ Subscribe\ to\ .*\ now\.' +STRING_UPGRADES_NON_UPGRADEABLE_CAPTION_RE='You\ are\ a\ .*\ subscriber!\ You\ have\ access\ to\ all\ our\ features\ until\ .*\.' +STRING_UPGRADES_CURRENT_PLAN_ENDED_CAPTION='Your subscription has ended, and you have limited access to all the features.' +STRING_UPGRADES_ERROR_FETCHING_DATA='Error while fetching plan details' +STRING_UPGRADES_PLAN_ENDED_NAME_RE='.*\ ended' +STRING_ORDER_SUBSCRIPTION='Subscription' +STRING_SUBSCRIPTION_ID_RE='Subscription\ \#.*' +STRING_SUBSCRIPTION_PERIOD_INTERVAL_SINGLE_RE='Every\ .*' +STRING_SUBSCRIPTION_PERIOD_INTERVAL_MULTIPLE_RE='Every\ .*\ .*' +STRING_PRODUCT_SUBSCRIPTION_DESCRIPTION_RE='.*\ every\ .*\ .*' +STRING_SUBSCRIPTION_NEVER_EXPIRE='Never expire' +STRING_SUBSCRIPTION_NO_TRIAL='No trial period' +STRING_SUBSCRIPTION_STATUS_ACTIVE='Active' +STRING_SUBSCRIPTION_STATUS_ON_HOLD='On Hold' +STRING_SUBSCRIPTION_STATUS_CANCELLED='Cancelled' +STRING_SUBSCRIPTION_STATUS_EXPIRED='Expired' +STRING_SUBSCRIPTION_STATUS_PENDING_CANCELLATION='Pending Cancellation' +STRING_SUBSCRIPTION_PERIOD_DAY='day' +STRING_SUBSCRIPTION_PERIOD_WEEK='week' +STRING_SUBSCRIPTION_PERIOD_MONTH='month' +STRING_SUBSCRIPTION_PERIOD_YEAR='year' +STRING_SUBSCRIPTION_PERIOD_MULTIPLE_DAYS='days' +STRING_SUBSCRIPTION_PERIOD_MULTIPLE_WEEKS='weeks' +STRING_SUBSCRIPTION_PERIOD_MULTIPLE_MONTHS='months' +STRING_SUBSCRIPTION_PERIOD_MULTIPLE_YEARS='years' +STRING_SUBSCRIPTION_EXPIRE='Expire after' +STRING_SUBSCRIPTION_SIGN_UP_FEE='Sign up fee' +STRING_SUBSCRIPTION_SIGN_UP_FEE_EXPLANATION='Optional, the sign-up fee will be charged immediately, even if the product has a free trial or the payment dates are synced.' +STRING_SUBSCRIPTION_FREE_TRIAL='Free trial' +STRING_SUBSCRIPTION_ONE_TIME_SHIPPING='One time shipping' +STRING_SUBSCRIPTION_ONE_TIME_SHIPPING_ENABLED='Enabled' +STRING_SUBSCRIPTION_ONE_TIME_SHIPPING_DESCRIPTION='Enable this to only charge shipping once on the initial order.' +STRING_SUBSCRIPTION_ONE_TIME_SHIPPING_NOTE='Note: for this setting to be enabled the subscription must not have a free trial or a synced renewal date.' +STRING_ORDER_GIFT_CARD='Gift Card' +STRING_GIFT_CARDS='Gift Cards' +STRING_PRODUCT_QUANTITY_RULES_TITLE='Quantity Rules' +STRING_MIN_QUANTITY='Minimum quantity' +STRING_MAX_QUANTITY='Maximum quantity' +STRING_GROUP_OF='Group of' +STRING_NO_QUANTITY_RULES='No Quantity Rules' +STRING_PRODUCT_BUNDLE='Bundled products' +STRING_PRODUCT_BUNDLE_SINGLE_COUNT='1 product' +STRING_PRODUCT_BUNDLE_MULTIPLE_COUNT_RE='.*\ products' +STRING_BUNDLED_PRODUCTS_INFO_NOTICE='You can edit bundled products in the web dashboard.' +STRING_MORE_MENU='Menu' +STRING_MORE_MENU_AVATAR='User profile picture' +STRING_MORE_MENU_SETTINGS_SECTION_TITLE='Settings' +STRING_MORE_MENU_GENERAL_SECTION_TITLE='General' +STRING_MORE_MENU_BUTTON_W_ADMIN='WC Admin' +STRING_MORE_MENU_BUTTON_WC_ADMIN_DESCRIPTION='Manage more on admin' +STRING_MORE_MENU_BUTTON_PAYMENTS='Payments' +STRING_MORE_MENU_BUTTON_PAYMENTS_DESCRIPTION='Take payments on the go' +STRING_MORE_MENU_BUTTON_AI_ASSISTANT='AI Assistant' +STRING_MORE_MENU_BUTTON_AI_ASSISTANT_DESCRIPTION='Ask about your store' +STRING_AI_ASSISTANT_VARIATION_CARD_ID_TITLE_RE='Variation\ .*' +STRING_MORE_MENU_BUTTON_GOOGLE='Google for WooCommerce' +STRING_MORE_MENU_BUTTON_GOOGLE_DESCRIPTION='Drive sales and generate more traffic with Google Ads' +STRING_MORE_MENU_BUTTON_BLAZE='Blaze' +STRING_MORE_MENU_BUTTON_BLAZE_DESCRIPTION='Promote products with Blaze' +STRING_MORE_MENU_BUTTON_INBOX='Inbox' +STRING_MORE_MENU_BUTTON_INBOX_DESCRIPTION='Stay up-to-date' +STRING_MORE_MENU_BUTTON_STORE='View Store' +STRING_MORE_MENU_BUTTON_STORE_DESCRIPTION='View your store' +STRING_MORE_MENU_BUTTON_COUPONS='Coupons' +STRING_MORE_MENU_BUTTON_COUPONS_DESCRIPTION='Boost sales with special offers' +STRING_MORE_MENU_BUTTON_CUSTOMERS='Customers' +STRING_MORE_MENU_BUTTON_CUSTOMERS_DESCRIPTION='Get customer insights' +STRING_MORE_MENU_BUTTON_REVIEWS='Reviews' +STRING_MORE_MENU_BUTTON_REVIEWS_DESCRIPTION='Capture reviews for your store' +STRING_MORE_MENU_BUTTON_SUBSCRIPTIONS='Subscriptions' +STRING_MORE_MENU_BUTTON_SUBSCRIPTIONS_DESCRIPTION='Manage your subscription' +STRING_MORE_MENU_BUTTON_SETTINGS='Settings' +STRING_MORE_MENU_BUTTON_SETTINGS_DESCRIPTION='Update your preferences' +STRING_MORE_MENU_BUTTON_WOO_POS='Point of Sale Mode' +STRING_MORE_MENU_BUTTON_WOO_POS_DESCRIPTION='Accept payments at your physical store' +STRING_MORE_MENU_CUSTOMERS_TITLE='Customers' +STRING_PRODUCT_COMPONENTS='Components' +STRING_PRODUCT_COMPONENT_SETTINGS='Component settings' +STRING_PRODUCT_COMPONENT_SINGLE_COUNT='1 component' +STRING_PRODUCT_COMPONENT_MULTIPLE_COUNT_RE='.*\ components' +STRING_COMPONENT_PRODUCTS_INFO_NOTICE='You can edit components in the web dashboard.' +STRING_COMPONENT_OPTIONS='Component options' +STRING_COMPONENT_DEFAULT_OPTION='Default option' +STRING_COMPONENT_TYPE_PRODUCTS='Products' +STRING_COMPONENT_TYPE_CATEGORIES='Categories' +STRING_SHIPPING_NOTICE_BANNER_WARNING_CONTENT='When shipping to countries that follow European Union (EU) customs rules, you must provide a clear, specific description of every item. Otherwise, shipments may be delayed or interrupted at customs.' +STRING_SHIPPING_NOTICE_BANNER_INSTRUCTIONS_CONTENT='Shipping to countries that follow European Union (EU) customs rules now requires you clearly describe every item. For example, if you are sending clothing, you must indicate what type of clothing (e.g., men\’s shirts, girl\’s vest, boy\’s jacket) for the description to be acceptable. Otherwise, shipments may be delayed or interrupted at customs.' +STRING_SHIPPING_NOTICE_LEARN_MORE='Learn More' +STRING_SHIPPING_NOTICE_DISMISS='Dismiss' +STRING_LOCAL_NOTIFICATION_BLAZE_NO_CAMPAIGN_REMINDER_TITLE='Boost your sales' +STRING_LOCAL_NOTIFICATION_BLAZE_NO_CAMPAIGN_REMINDER_DESCRIPTION='Promote your products with Blaze Ads and increase your sales now.' +STRING_LOCAL_NOTIFICATION_BLAZE_ABANDONED_CAMPAIGN_REMINDER_TITLE='Thinking about boosting your sales?' +STRING_LOCAL_NOTIFICATION_BLAZE_ABANDONED_CAMPAIGN_REMINDER_DESCRIPTION='Get your products seen by millions with Blaze and boost your sales' +STRING_LOCAL_NOTIFICATION_WOO_POS_SURVEY_POTENTIAL_USER_TITLE='Thinking about in-person sales?' +STRING_LOCAL_NOTIFICATION_WOO_POS_SURVEY_POTENTIAL_USER_DESCRIPTION='Take a quick 2-minute survey to help us shape features you'"'"'ll love.' +STRING_LOCAL_NOTIFICATION_WOO_POS_SURVEY_CURRENT_USER_TITLE='How'"'"'s POS working for you?' +STRING_LOCAL_NOTIFICATION_WOO_POS_SURVEY_CURRENT_USER_DESCRIPTION='Share your experience in a quick 2-minute survey and help us improve.' +STRING_POS_CLIENT_SIDE_BANNER_TITLE='Run WooCommerce POS on tablets' +STRING_POS_CLIENT_SIDE_BANNER_DESCRIPTION='Take in‑person payments with WooCommerce POS. Set up on a tablet and start selling today.' +STRING_POS_CLIENT_SIDE_BANNER_CTA='Learn more' +STRING_FIRST_PRODUCT_CELEBRATION_TITLE='First product created 🎉' +STRING_FIRST_PRODUCT_CELEBRATION_BODY_MESSAGE='Congratulations! You'"'"'re one step closer to get the new store ready.' +STRING_SHARE_PRODUCT='Share product' +STRING_PRODUCT_SHARING_WRITE_WITH_AI='Write with AI' +STRING_PRODUCT_SHARING_REGENERATE='Write again' +STRING_PRODUCT_SHARING_GENERATING='Writing…' +STRING_PRODUCT_SHARING_OPTIONAL_MESSAGE_LABEL='Add an optional message' +STRING_PRODUCT_SHARING_AI_GENERATING_FAILURE='Unable to generate sharing message. Please try again!' +STRING_AI_PRODUCT_DESCRIPTION_TITLE='Write a description' +STRING_AI_PRODUCT_DESCRIPTION_EXAMPLE='Example: Potted, Cactus, Plant, Decorative, Easy-care' +STRING_AI_PRODUCT_DESCRIPTION_HINT='Highlight your product'"'"'s unique features and audience with keywords for a tailored description.' +STRING_AI_PRODUCT_DESCRIPTION_TITLE_HINT='Enter product title.' +STRING_AI_PRODUCT_DESCRIPTION_REGENERATE_BUTTON='Regenerate' +STRING_AI_PRODUCT_DESCRIPTION_FEEDBACK='Is the generated\ndescription helpful?' +STRING_AI_PRODUCT_DESCRIPTION_NOTE_DIALOG_HEADING='Great start!' +STRING_AI_PRODUCT_DESCRIPTION_NOTE_DIALOG_MESSAGE='Please keep in mind that this product description was generated using our AI-powered tool. Please review and edit the content to ensure it aligns with your brand and messaging.' +STRING_AI_PRODUCT_DESCRIPTION_NOTE_DIALOG_CONFIRMATION='Got it' +STRING_AI_PRODUCT_DESCRIPTION_LEARN_MORE_LINK='Powered by AI. Learn more.' +STRING_AI_PRODUCT_DESCRIPTION_LABEL='Description generated by AI' +STRING_AI_PRODUCT_DESCRIPTION_ERROR='There was a problem generating the product description. Please try again later.' +STRING_AI_PRODUCT_TOOLBAR_BUTTON_TOOLTIP='Generate a description using AI' +STRING_AI_PRODUCT_DESCRIPTION_TOOLTIP_TITLE='✨ Write with AI' +STRING_AI_PRODUCT_DESCRIPTION_TOOLTIP_MESSAGE='Use our AI-powered tool to quickly generate product descriptions. Just input keywords and we'"'"'ll do the rest!' +STRING_AI_PRODUCT_DESCRIPTION_TOOLTIP_DISMISS='Got it' +STRING_AI_PRODUCT_DESCRIPTION_COPY_SUCCESS='Product description copied to clipboard.' +STRING_AI_PRODUCT_DESCRIPTION_COPY_ERROR='Error copying product description to clipboard.' +STRING_AI_ORDER_THANK_YOU_NOTE_DIALOG_TITLE='Thank-you note' +STRING_AI_ORDER_THANK_YOU_NOTE_DIALOG_LOADING_MESSAGE='✨ Creating a thank-you note for your order…' +STRING_AI_ORDER_THANK_YOU_NOTE_DIALOG_REGENERATE_BUTTON='Regenerate' +STRING_AI_ORDER_THANK_YOU_NOTE_DIALOG_SHARE_BUTTON='Share' +STRING_AI_ORDER_THANK_YOU_NOTE_DIALOG_FAILED_TITLE='Unable to create Thank-you note. Please use the button below to retry.' +STRING_AI_ORDER_THANK_YOU_NOTE_COPY_LABEL='Thank-you note generated by AI' +STRING_BLAZE_CAMPAIGN_TITLE='Blaze campaign' +STRING_BLAZE_CAMPAIGN_SUBTITLE='Increase visibility and get your products sold quickly.' +STRING_BLAZE_CAMPAIGN_CREATE_CAMPAIGN_BUTTON='Create campaign' +STRING_BLAZE_CAMPAIGN_SUGGESTED_PRODUCT_CAPTION='Suggested product' +STRING_BLAZE_CAMPAIGN_SHOW_ALL_BUTTON='View all campaigns' +STRING_BLAZE_CAMPAIGN_STATUS_IN_MODERATION='In moderation' +STRING_BLAZE_CAMPAIGN_STATUS_ACTIVE='Active' +STRING_BLAZE_CAMPAIGN_STATUS_COMPLETED='Completed' +STRING_BLAZE_CAMPAIGN_STATUS_SCHEDULED='Scheduled' +STRING_BLAZE_CAMPAIGN_STATUS_REJECTED='Rejected' +STRING_BLAZE_CAMPAIGN_STATUS_CANCELED='Canceled' +STRING_BLAZE_CAMPAIGN_STATUS_SUSPENDED='Suspended' +STRING_BLAZE_CAMPAIGN_STATUS_CTR_LABEL='Click-throughs' +STRING_BLAZE_CAMPAIGN_STATUS_CTR_VALUE_SHORTENED_RE='.*\ ➔\ .*' +STRING_BLAZE_CAMPAIGN_STATUS_BUDGET_TOTAL='Total' +STRING_BLAZE_CAMPAIGN_STATUS_BUDGET_REMAINING='Remaining' +STRING_BLAZE_CAMPAIGN_STATUS_BUDGET_WEEKLY='Weekly' +STRING_BLAZE_CAMPAIGN_LIST_TITLE='Blaze campaigns' +STRING_BLAZE_CAMPAIGN_LIST_ERROR_FETCHING_CAMPAIGNS='There was an error refreshing the list of campaigns. Please try again later.' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_TITLE='Get your products seen by millions' +STRING_BLAZE_CAMPAIGN_CREATION_CELEBRATION_HEADER='All set!' +STRING_BLAZE_CAMPAIGN_CEATION_CELEBRATION_MESSAGE='The ad has been submitted for approval. We’ll send you a confirmation email once it’s approvied and running.' +STRING_BLAZE_CAMPAIGN_CEATION_CELEBRATION_BUTTON='Got it' +STRING_BLAZE_CAMPAIGN_SCREEN_FRAGMENT_TITLE='Preview' +STRING_BLAZE_CAMPAIGN_PREVIEW_EDIT_AD='Edit ad' +STRING_BLAZE_CAMPAIGN_PREVIEW_DETAILS_SECTION_TITLE='Details' +STRING_BLAZE_CAMPAIGN_PREVIEW_AUDIENCE_SECTION_TITLE='Audience' +STRING_BLAZE_CAMPAIGN_PREVIEW_PRODUCT_IMAGE_CONTENT_DESCRIPTION='Product image used for the ad' +STRING_BLAZE_CAMPAIGN_PREVIEW_DETAILS_BUDGET='Budget' +STRING_BLAZE_CAMPAIGN_PREVIEW_DETAILS_OBJECTIVE='Campaign objective' +STRING_BLAZE_CAMPAIGN_PREVIEW_DETAILS_CHOOSE_OBJECTIVE='Choose campaign objective' +STRING_BLAZE_CAMPAIGN_PREVIEW_DETAILS_LANGUAGE='Language' +STRING_BLAZE_CAMPAIGN_PREVIEW_DETAILS_DEVICES='Devices' +STRING_BLAZE_CAMPAIGN_PREVIEW_DETAILS_LOCATION='Location' +STRING_BLAZE_CAMPAIGN_PREVIEW_DETAILS_INTERESTS='Interests' +STRING_BLAZE_CAMPAIGN_PREVIEW_DETAILS_DESTINATION_URL='Ad destination' +STRING_BLAZE_CAMPAIGN_PREVIEW_DETAILS_CONFIRM_DETAILS_BUTTON='Confirm Details' +STRING_BLAZE_CAMPAIGN_PREVIEW_DAYS_DURATION_RE='.*\ days\ from\ .*' +STRING_BLAZE_CAMPAIGN_PREVIEW_DAYS_DURATION_ENDLESS_RE='.*\ weekly,\ starting\ from\ .*' +STRING_BLAZE_CAMPAIGN_PREVIEW_TARGET_DEFAULT_VALUE='All' +STRING_BLAZE_CAMPAIGN_PREVIEW_TARGET_LOCATION_SEARCH_HINT='Search locations' +STRING_BLAZE_CAMPAIGN_PREVIEW_MISSING_IMAGE_DIALOG_TEXT='Please add an image for the Blaze campaign' +STRING_BLAZE_CAMPAIGN_PREVIEW_MISSING_IMAGE_DIALOG_POSITIVE_BUTTON='Add Image' +STRING_BLAZE_CAMPAIGN_PREVIEW_MISSING_CONTENT_DIALOG_TEXT='Please add a tagline and a description for your Blaze campaign' +STRING_BLAZE_CAMPAIGN_PREVIEW_MISSING_CONTENT_DIALOG_POSITIVE_BUTTON='Add' +STRING_BLAZE_CAMPAIGN_PREVIEW_MISSING_OBJECTIVE_DIALOG_TEXT='Please select an objective for the Blaze campaign' +STRING_BLAZE_CAMPAIGN_PREVIEW_MISSING_OBJECTIVE_DIALOG_POSITIVE_BUTTON='Select objective' +STRING_BLAZE_CAMPAIGN_PREVIEW_TOS_CHECKBOX_LESS_THAN_7_DAYS_CAMPAIGN_RE='I\ agree\ to\ be\ charged\ up\ to\ .*\ starting\ .*\.\ Charges\ may\ occur\ in\ one\ or\ more\ payments\ while\ the\ campaign\ is\ active\.\ I\ can\ cancel\ anytime;\ I\\’ll\ only\ pay\ for\ ads\ delivered\ up\ to\ cancellation\.' +STRING_BLAZE_CAMPAIGN_PREVIEW_TOS_CHECKBOX_OVER_7_DAYS_CAMPAIGN_RE='I\ agree\ to\ a\ recurring\ charge\ of\ up\ to\ .*\ weekly,\ starting\ .*\.\ Charges\ may\ occur\ at\ varying\ times\ during\ the\ campaign\.\ I\ can\ cancel\ anytime;\ I\\’ll\ only\ pay\ for\ ads\ delivered\ up\ to\ cancellation\.' +STRING_BLAZE_CAMPAIGN_PREVIEW_TOS_CHECKBOX_EVERGREEN_CAMPAIGNS_RE='I\ agree\ to\ a\ recurring\ weekly\ charge\ up\ to\ .*\ starting\ .*\.\ Charges\ may\ occur\ at\ varying\ times\ during\ the\ campaign\.\ I\ can\ cancel\ anytime;\ I\\’ll\ only\ pay\ for\ ads\ delivered\ up\ to\ cancellation\.' +STRING_BLAZE_CAMPAIGN_OBJECTIVE_SELECT_OBJECTIVE_LABEL_RE='Select\ objective\ .*' +STRING_BLAZE_CAMPAIGN_OBJECTIVE_GOOD_FOR_RE='Good\ for:\ .*' +STRING_BLAZE_CAMPAIGN_OBJECTIVE_SAVE_SELECTION_SWITCH_LABEL='Save my selection for future campaigns' +STRING_BLAZE_CAMPAIGN_BUDGET_TOOLBAR_TITLE='Set your budget' +STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_SUBTITLE='How much would you like to spend on your campaign, and how long should it run for?' +STRING_BLAZE_CAMPAIGN_BUDGET_FOOTER_WEEKLY_SPEND='weekly spend' +STRING_BLAZE_CAMPAIGN_BUDGET_DAILY_SPEND_LABEL='Daily spend' +STRING_BLAZE_CAMPAIGN_BUDGET_DAYS_DURATION_RE='for\ .*\ days' +STRING_BLAZE_CAMPAIGN_BUDGET_REACH_FORECAST='Estimated people reached per day' +STRING_BLAZE_CAMPAIGN_BUDGET_SCHEDULED_SECTION_TITLE='Schedule' +STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_ENDLESS_CAMPAIGN_VALUE_RE='Ongoing\ from\ .*' +STRING_BLAZE_CAMPAIGN_BUDGET_EDIT_DURATION_BUTTON='Edit' +STRING_BLAZE_CAMPAIGN_BUDGET_UPDATE_BUTTON='Update' +STRING_BLAZE_CAMPAIGN_BUDGET_IMPRESSIONS_TITLE='Impressions' +STRING_BLAZE_CAMPAIGN_BUDGET_IMPRESSIONS_DONE_BUTTON='Done' +STRING_BLAZE_CAMPAIGN_BUDGET_IMPRESSIONS_INFO='Impressions reflect the frequency with which your ad appears to potential customers.\n\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\n Remember, impressions are about visibility, not action taken by viewers.' +STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_BOTTOM_SHEET_DURATION_RE='.*\ days' +STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_BOTTOM_SHEET_END_DATE_RE='to\ .*' +STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_BOTTOM_SHEET_START_DATE='Start date' +STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_ENDLESS_SWITCH_LABEL='Specify the duration' +STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_ENDLESS_DESCRIPTION='Campaign will run until you stop it.' +STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_CURRENT_DURATION='Duration' +STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_BOTTOM_SHEET_APPLY_BUTTON='Apply' +STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_CANCEL_BUTTON='Cancel' +STRING_BLAZE_CAMPAIGN_BUDGET_ERROR_FETCHING_FORECAST='Failed to estimate impressions. Retry?' +STRING_BLAZE_CAMPAIGN_BUDGET_WEEKLY_SPENDING_RE='.*\ weekly' +STRING_BLAZE_CAMPAIGN_EDIT_AD_CHANGE_IMAGE_BUTTON='Change image' +STRING_BLAZE_CAMPAIGN_EDIT_AD_CHANGE_TAGLINE_TITLE='Tagline' +STRING_BLAZE_CAMPAIGN_EDIT_AD_CHANGE_TAGLINE_EMPTY_ERROR='Tagline cannot be empty' +STRING_BLAZE_CAMPAIGN_EDIT_AD_CHANGE_CTA_TEXT_TITLE='Call to action' +STRING_BLAZE_CAMPAIGN_EDIT_AD_CHANGE_DESCRIPTION_TITLE='Description' +STRING_BLAZE_CAMPAIGN_EDIT_AD_CHANGE_DESCRIPTION_EMPTY_ERROR='Description cannot be empty' +STRING_BLAZE_CAMPAIGN_EDIT_AD_CHARACTERS_REMAINING_RE='.*\ characters\ remaining' +STRING_BLAZE_CAMPAIGN_EDIT_AD_SUGGESTED_BY_AI='Suggested by AI' +STRING_BLAZE_CAMPAIGN_EDIT_AD_INVALID_IMAGE_TITLE='Invalid image' +STRING_BLAZE_CAMPAIGN_EDIT_AD_INVALID_IMAGE_DESCRIPTION='Please select an image with a minimum size of 400x400 pixels' +STRING_BLAZE_CAMPAIGN_EDIT_AD_UNSUPPORTED_IMAGE_TYPE_DESCRIPTION='This image type is not supported. Please select a PNG, JPEG, WebP, GIF, BMP, HEIC, or HEIF image.' +STRING_BLAZE_CAMPAIGN_PRODUCT_PHOTO_PICKER_TITLE='Product Photos' +STRING_BLAZE_CAMPAIGN_PRODUCT_PHOTO_PICKER_EMPTY='No photos found' +STRING_BLAZE_CAMPAIGN_PRODUCT_PHOTO_PICKER_PHOTO_CONTENT_DESCRIPTION='Product photo' +STRING_BLAZE_CAMPAIGN_EDIT_AD_ARROW_FORWARD_DESCRIPTION='Next suggestion' +STRING_BLAZE_CAMPAIGN_EDIT_AD_ARROW_BACK_DESCRIPTION='Previous suggestion' +STRING_BLAZE_CAMPAIGN_PAYMENT_SUMMARY_TITLE='Payment' +STRING_BLAZE_CAMPAIGN_PAYMENT_SUMMARY_TOTALS='Payment totals' +STRING_BLAZE_CAMPAIGN_PAYMENT_SUMMARY_CAMPAIGN_BUDGET='Blaze campaign' +STRING_BLAZE_CAMPAIGN_PAYMENT_SUMMARY_TOTAL_BUDGET='Total' +STRING_BLAZE_CAMPAIGN_PAYMENT_SUMMARY_LOADING_PAYMENT_METHODS='Loading payment methods' +STRING_BLAZE_CAMPAIGN_PAYMENT_SUMMARY_ADD_PAYMENT_METHOD='Add payment method' +STRING_BLAZE_CAMPAIGN_PAYMENT_SUMMARY_ERROR_LOADING_PAYMENT_METHODS='Loading payment methods failed, please retry by clicking here!' +STRING_BLAZE_CAMPAIGN_PAYMENT_SUMMARY_SUBMIT_CAMPAIGN='Submit campaign' +STRING_BLAZE_CAMPAIGN_PAYMENT_SUMMARY_TERMS_AND_CONDITIONS='By clicking "Submit campaign" you agree to the Terms of Service and Advertising Policy, and authorize your payment method to be charged for the budget and duration you chose. Learn more about how budgets and payments for Promoted Posts work.' +STRING_BLAZE_CAMPAIGN_PAYMENT_LIST_SCREEN_TITLE='Payment method' +STRING_BLAZE_CAMPAIGN_PAYMENT_LIST_EMPTY_STATE_TEXT='Please add a new payment method' +STRING_BLAZE_CAMPAIGN_PAYMENT_LIST_ADD_PAYMENT_METHOD_BUTTON='Add credit card' +STRING_BLAZE_CAMPAIGN_PAYMENT_LIST_HEADER_TEXT='All transactions are secure and encrypted' +STRING_BLAZE_CAMPAIGN_PAYMENT_LIST_HINT_RE='Credits\ cards\ are\ retrieved\ from\ the\ following\ WordPress\.com\ account:\ .*\ <.*>' +STRING_BLAZE_CAMPAIGN_PAYMENT_LIST_ADD_NEW_PAYMENT_METHOD_BUTTON='Add new card' +STRING_BLAZE_CAMPAIGN_PAYMENT_ADDED_SUCCESSFULLY='Credit card added successfully' +STRING_BLAZE_CAMPAIGN_DETAILS_TITLE='Campaign details' +STRING_PRODUCT_DETAILS_BLAZE_CARD='Promote with Blaze' +STRING_BLAZE_CAMPAIGN_CREATION_PRODUCT_SELECTOR_TITLE='Ready to promote' +STRING_BLAZE_CAMPAIGN_CREATION_PRODUCT_SELECTOR_CTA_BUTTON='Promote' +STRING_BLAZE_CAMPAIGN_CREATION_LOCATION_SEARCH_MESSAGE='Start typing country, state or city to see available options' +STRING_BLAZE_CAMPAIGN_CREATION_LOCATION_SEARCH_FAILED_MESSAGE='Searching failed.\nPlease try again' +STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_EMPTY_PARAMETERS_MESSAGE='Enter manually' +STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_URL_PROPERTY_TITLE='Destination URL' +STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_PARAMETERS_PROPERTY_TITLE='URL parameters' +STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_PRODUCT_URL_OPTION='The product URL' +STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_SITE_URL_OPTION='The site home' +STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_ADD_PARAMETER_BUTTON='Add parameter' +STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_DESTINATION_WITH_PARAMETERS_RE='Destination:\ .*' +STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_PARAMETER_KEY='Key' +STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_PARAMETER_VALUE='Value' +STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_TOO_LONG_ERROR='The final URL is too long' +STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_KEY_EXISTS_ERROR='The key already exits' +STRING_BLAZE_CAMPAIGN_CREATED_SUCCESS_TITLE='Ready to Go!' +STRING_BLAZE_CAMPAIGN_CREATED_SUCCESS_DESCRIPTION='We'"'"'re reviewing your campaign. It'"'"'ll be live within 24 hours. Exciting times ahead for your sales!' +STRING_BLAZE_CAMPAIGN_CREATED_SUCCESS_DONE_BUTTON='Done' +STRING_BLAZE_CAMPAIGN_CREATED_SUCCESS_FEEDBACK_REQUEST='How was the experience with Blaze' +STRING_BLAZE_CAMPAIGN_CREATION_LOADING='Creating your campaign' +STRING_BLAZE_CAMPAIGN_CREATION_ERROR_TITLE='Error creating campaign' +STRING_BLAZE_CAMPAIGN_CREATION_ERROR_PAYMENT_HINT='Error creating campaign' +STRING_BLAZE_CAMPAIGN_CREATION_ERROR_HELP_HINT='Please try again, or contact support for assistance.' +STRING_BLAZE_CAMPAIGN_CREATION_ERROR_GET_SUPPORT='Get support' +STRING_BLAZE_CAMPAIGN_CREATION_ERROR_CANCEL='Cancel campaign' +STRING_BLAZE_CAMPAIGN_CREATION_ERROR_MEDIA_UPLOAD='Failed to upload campaign image.' +STRING_BLAZE_CAMPAIGN_CREATION_ERROR_MEDIA_FETCH='Failed to fetch campaign image details' +STRING_BLAZE_CAMPAIGN_CREATION_ERROR='Something’s not quite right.\nWe couldn'"'"'t create your campaign.' +STRING_HIGHLIGHTS_TOOLTIP_TITLE_HINT='Tooltip title' +STRING_HIGHLIGHTS_TOOLTIP_MESSAGE_HINT='Tooltip message. \n This can be multiple lines.' +STRING_HIGHLIGHTS_TOOLTIP_BUTTON_HINT='Button text' +STRING_TAX_RATES_INFO_DIALOG_PRIMARY_TEXT='Taxes are calculated by matching your customer’s billing or shipping address, or your shop address to a tax rate location.' +STRING_TAX_RATES_INFO_DIALOG_SECONDARY_TEXT='Tax rates for different locations can be managed in your store’s admin.' +STRING_TAX_RATES_INFO_DIALOG_TITLE='Taxes & Tax Rates' +STRING_TAX_RATES_REDIRECT_TO_ADMIN_CONTENT_DESCRIPTION='Edit Tax Rates in Admin Button' +STRING_TAX_RATES_REDIRECT_TO_ADMIN_BUTTON_LABEL='Edit Tax Rates in Admin' +STRING_TAX_RATES_INFO_DIALOG_TAX_BASED_ON_STORE_ADDRESS_RE='Your\ tax\ rate\ is\ currently\ calculated\ based\ on\ your\ shop\ address.*' +STRING_TAX_RATES_INFO_DIALOG_TAX_BASED_ON_BILLING_ADDRESS_RE='Your\ tax\ rate\ is\ currently\ calculated\ based\ on\ your\ billing\ address.*' +STRING_TAX_RATES_INFO_DIALOG_TAX_BASED_ON_SHIPPING_ADDRESS_RE='Your\ tax\ rate\ is\ currently\ calculated\ based\ on\ your\ shipping\ address.*' +STRING_TAX_RATE_SELECTOR_FOOTER_LABEL='Can'"'"'t find the rate you'"'"'re looking for?' +STRING_TAX_RATE_SELECTOR_LIST_HEADER='SELECT A TAX RATE' +STRING_TAX_RATE_SELECTOR_INFO_ICON_CONTENT_DESCRIPTION='Button opening tax rates info dialog' +STRING_TAX_RATE_SELECTOR_HEADER_LABEL='This will change the customer’s address to the location of the tax rate you select.' +STRING_TAX_RATE_SELECTOR_EDIT_RATES_BUTTON_LABEL='Edit tax rates in admin' +STRING_TAX_RATE_SELECTOR_TITLE='Set Tax Rate' +STRING_TAX_RATE_SELECTOR_EMPTY_LIST_TITLE='We couldn'"'"'t find any tax rates' +STRING_TAX_RATE_SELECTOR_EMPTY_LIST_MESSAGE='Add tax rates in admin. Only tax rates with location information will be shown here.' +STRING_TAX_RATE_SELECTOR_EMPTY_LIST_BUTTON='Edit Tax Rates in Admin' +STRING_TAX_RATE_SELECTOR_EMPTY_LIST_BUTTON_ALT='Edit Tax Rates' +STRING_TAX_RATE_SELECTOR_AUTO_RATE_LABEL='Add this rate to all created orders' +STRING_TAX_RATE_SELECTOR_AUTO_RATE_SUBTITLE='This will not affect online orders' +STRING_TAX_RATE_SELECTOR_AUTO_RATE_DETAILS_TITLE='Automatically adding tax rate' +STRING_TAX_RATE_SELECTOR_AUTO_RATE_DETAILS_SET_A_NEW_RATE_BUTTON_LABEL='Set a new tax rate for this order' +STRING_TAX_RATE_SELECTOR_AUTO_RATE_DETAILS_CLEAR_BUTTON_LABEL='Clear address and stop using this rate' +STRING_NEXT_ORDER='Next order' +STRING_PREVIOUS_ORDER='Previous order' +STRING_AI_FEEDBACK_FORM_MESSAGE='Is the result helpful?' +STRING_AI_FEEDBACK_FORM_POSITIVE_BUTTON='Positive feedback' +STRING_AI_FEEDBACK_FORM_NEGATIVE_BUTTON='Negative feedback' +STRING_LOGIN_TEXT_SECURITY_KEY='Use a security key' +STRING_LOGIN_ERROR_SECURITY_KEY='There was some trouble with the Security key login' +STRING_NOTIFICATION_SECURITY_KEY_NEEDED='Please provide your security key to continue.' +STRING_SCAN_TO_UPDATE_INVENTORY_UNABLE_TO_FIND_PRODUCT_RE='Product\ with\ SKU:\ .*\ not\ found\.\ Please\ try\ again\.' +STRING_SCAN_TO_UPDATE_INVENTORY_INCREMENT_QUANTITY_BUTTON='Quantity + 1' +STRING_SCAN_TO_UPDATE_INVENTORY_UPDATE_QUANTITY_BUTTON='Update Quantity' +STRING_SCAN_TO_UPDATE_INVENTORY_PRODUCT_DETAILS_BUTTON='View Product Details' +STRING_SCAN_TO_UPDATE_INVENTORY_SUCCESS_SNACKBAR_RE='Quantity\ updated:\ .*' +STRING_SCAN_TO_UPDATE_INVENTORY_UNDO_SNACKBAR='Update Quantity Undone' +STRING_SCAN_TO_UPDATE_INVENTORY_FAILURE_SNACKBAR='Something went wrong. Please try again' +STRING_SCAN_TO_UPDATE_INVENTORY_ORIGINAL_QUANTITY_LABEL='Original quantity' +STRING_SCAN_TO_UPDATE_INVENTORY_QUANTITY_LABEL='Quantity' +STRING_SCAN_TO_UPDATE_INVENTORY_PRODUCT_LABEL='Product' +STRING_SCAN_TO_UPDATE_INVENTORY_STOCK_NOT_MANAGED='Stock not managed' +STRING_SCAN_TO_UPDATE_INVENTORY_MANAGE_STOCK='Manage Stock' +STRING_EXTENSION_CONFIGURE_BUTTON='Configure' +STRING_DEFAULT_PRODUCT_TITLE_RE='Product\ .*' +STRING_PRODUCT_CONFIGURATION_TITLE='Configuration' +STRING_SAVE_CONFIGURATION='Save configuration' +STRING_ORDER_CONFIGURATION_CHANGE_PRODUCT_QUANTITY='Change the product quantity from %1$.2f to %2$.2f' +STRING_CONFIGURATION_REQUIRED='Configuration required' +STRING_CONFIGURATION_COMPLETE='Configuration complete' +STRING_CONFIGURATION_QUANTITY_ITEM_RE='.*\ item' +STRING_CONFIGURATION_QUANTITY_ITEM_PLURAL_RE='.*\ items' +STRING_CONFIGURATION_QUANTITY_BETWEEN_RE='between\ .*\ and\ .*\ items' +STRING_CONFIGURATION_QUANTITY_LESS_THAN_RE='less\ than\ .*\ items' +STRING_CONFIGURATION_QUANTITY_MORE_THAN_RE='more\ than\ .*\ item' +STRING_CONFIGURATION_QUANTITY_MORE_THAN_PLURAL_RE='more\ than\ .*\ items' +STRING_CONFIGURATION_QUANTITY_RULE_ISSUE_RE='Please\ select\ .*' +STRING_CONFIGURATION_VARIABLE_SELECTION='please select a variation' +STRING_CONFIGURATION_CHILDREN_ISSUE_RE='".*\ "\ \->\ .*' +STRING_CONFIGURATION_VARIABLE_UPDATE='Choose variation' +STRING_PRODUCT_VARIATION_PICKER_TITLE='Select a variation' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_DESCRIPTION='Our tool is designed to empower merchants with fast, simple ad campaign setups for maximum traffic boost.' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_BENEFIT_1_TITLE='Quick start, big impact' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_BENEFIT_1_SUBTITLE_UPDATED='Launch ads in minutes – no experience or big budget needed, starting at just $5 USD daily.' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_BENEFIT_2_TITLE='Global reach made simple' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_BENEFIT_2_SUBTITLE='"Our tool presents your product where interested shoppers can find it. "' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_BENEFIT_3_TITLE='Access a vast audience' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_BENEFIT_3_SUBTITLE='Your ads on millions of sites within the WordPress.com and Tumblr networks.' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_START_BUTTON='Start your campaign' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_LEARN_MORE='Learn how Blaze works' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_LEARN_ITEM_1='Choose a product: Choose what to promote with Blaze.' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_LEARN_ITEM_2='Customize targeting: Select audience by location or interests, and see potential reach.' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_LEARN_ITEM_3='Set your budget: Decide on your spend and campaign length.' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_LEARN_ITEM_4='Quick review: Submit your ad for a fast moderator check.' +STRING_BLAZE_CAMPAIGN_CREATION_NEW_INTRO_LEARN_ITEM_5='Go live: Watch as your promotion begins and track its success.' +STRING_BLAZE_CAMPAIGN_CREATION_NO_PRODUCT_MODAL_TITLE='No products found' +STRING_BLAZE_CAMPAIGN_CREATION_NO_PRODUCT_MODAL_BODY='You currently don'"'"'t have a product available for promotion. Would you like to create a product now?' +STRING_BLAZE_CAMPAIGN_CREATION_NO_PRODUCT_MODAL_CTA='Create Product' +STRING_ORDER_CONFIGURATION_PRODUCT_SELECTION_RE='Select\ product\ .*' +STRING_CARD_SELECTION_CONTROL='Analytic card selection' +STRING_DISABLED='Disabled' +STRING_SEE_REPORT='See report' +STRING_PRODUCT_UPDATE_STOCK_STATUS_TITLE='Update Stock Status' +STRING_PRODUCT_UPDATE_STOCK_STATUS_DONE='DONE' +STRING_PRODUCT_UPDATE_STOCK_STATUS_CURRENT_STATUS_MIXED='Current stock statuses are mixed' +STRING_PRODUCT_UPDATE_STOCK_STATUS_CURRENT_STATUS_SINGLE_RE='Current\ stock\ status\ is\ .*' +STRING_PRODUCT_UPDATE_STOCK_STATUS_UPDATE_COUNT_RE='Stock\ status\ will\ be\ updated\ for\ .*\ products\.' +STRING_PRODUCT_UPDATE_STOCK_STATUS_IGNORED_COUNT_RE='.*\ products\ with\ managed\ stock\ quantity\ will\ be\ ignored\.' +STRING_PRODUCT_UPDATE_STOCK_STATUS_VARIABLE_IGNORED_COUNT_RE='.*\ variable\ products\ will\ be\ ignored\.' +STRING_PRODUCT_UPDATE_STOCK_STATUS_UPDATE_COUNT_SINGULAR='Stock status will be updated for 1 product.' +STRING_PRODUCT_UPDATE_STOCK_STATUS_IGNORED_COUNT_SINGULAR='1 product with managed stock quantity will be ignored.' +STRING_PRODUCT_UPDATE_STOCK_STATUS_VARIABLE_IGNORED_COUNT_SINGULAR='1 variable product will be ignored.' +STRING_PRODUCT_UPDATE_STOCK_STATUS_MANAGED_PRODUCTS='Managed products can'"'"'t be updated' +STRING_PRODUCT_UPDATE_STOCK_STATUS_VARIABLE_PRODUCTS='Variable products can'"'"'t be updated' +STRING_PRODUCT_UPDATE_STOCK_STATUS_COMPLETED='Updated stock statuses' +STRING_PRODUCT_UPDATE_STOCK_STATUS_ERROR='Something went wrong. Please try again.' +STRING_PRODUCT_UPDATE_STOCK_STATUS_DIALOG_TITLE='Updating stock statuses' +STRING_PRODUCT_UPDATE_STOCK_STATUS_DIALOG_SUBTITLE='Please wait…' +STRING_EMPTY_ORDER_DETAIL_TITLE='No Order Details Yet' +STRING_EMPTY_ORDER_DETAIL_MESSAGE='Your order details will show here when you'"'"'ve made an order.' +STRING_LOGIN_APP_PASSWORD_TITLE='We couldn'"'"'t log in into your store' +STRING_LOGIN_APP_PASSWORD_SUBTITLE='This could because your store has some extra security steps in place.' +STRING_LOGIN_APP_PASSWORD_INSTRUCTIONS_TITLE='Follow these steps to connect the Woo app directly to your store using an application password.' +STRING_LOGIN_APP_PASSWORD_INSTRUCTIONS_STEP_1='1. First, log in using your site credentials.' +STRING_LOGIN_APP_PASSWORD_INSTRUCTIONS_STEP_2='2. When prompted, approve the connection by tapping the confirmation button.' +STRING_LOGIN_APP_PASSWORD_INSTRUCTIONS_STEP_3='3. When the connection is complete, you will be logged in to your store.' +STRING_LOGIN_APP_PASSWORD_INSTRUCTIONS_FOOTER='If you run into any issues, please contact our support team.' +STRING_LOGIN_APP_PASSWORD_CONTINUE_BUTTON='Continue' +STRING_LOGIN_APP_PASSWORD_SUPPORT_BUTTON='Contact Support' +STRING_LOGIN_APP_PASSWORD_EXIT_DIALOG_MESSAGE='It seems that you have not approved the app connection yet. Are you sure you want to exit?' +STRING_LOGIN_APP_PASSWORD_EXIT_DIALOG_CONFIRMATION='Exit Anyway' +STRING_LOGIN_APP_PASSWORD_EXIT_DIALOG_CANCEL='Cancel' +STRING_WOOPOS_HOME_SYNCING_CATALOG_TITLE='Syncing catalog' +STRING_WOOPOS_HOME_SYNCING_CATALOG_PREPARING='Preparing catalog…' +STRING_WOOPOS_HOME_SYNCING_CATALOG_PROGRESS_RE='.*\ of\ .*\ items' +STRING_WOOPOS_HOME_SYNCING_CATALOG_HINT='Catalog syncing may take a few minutes.' +STRING_WOOPOS_HOME_SYNCING_CATALOG_SUBTITLE='You can leave and syncing will continue in the background.' +STRING_WOOPOS_HOME_SYNCING_CATALOG_EXIT_BUTTON='Exit POS' +STRING_WOOPOS_HOME_SYNC_FAILED_TITLE='Unable to sync' +STRING_WOOPOS_HOME_SYNC_FAILED_BLOCKED_TITLE='Action needed on your store' +STRING_WOOPOS_HOME_SYNC_FAILED_MESSAGE='We are unable to sync your product catalog. Please check your internet connection and retry.' +STRING_WOOPOS_HOME_SYNC_FAILED_SERVER_PERMISSIONS_MESSAGE='Point of Sale can'"'"'t access a file with your product information because your hosting provider is blocking it. Please ask them to allow access to it so Point of Sale keeps working properly.' +STRING_WOOPOS_HOME_SYNC_FAILED_RETRY_BUTTON='Retry' +STRING_WOOPOS_HOME_SYNC_FAILED_BLOCKED_CONTINUE_BUTTON='Got it' +STRING_WOOPOS_REFRESH_CATALOG_BANNER_TITLE='Refresh catalog' +STRING_WOOPOS_REFRESH_CATALOG_BANNER_MESSAGE='The catalog hasn'"'"'t been synced in the last 7 days. Either connect your device to WiFi or enable syncing over cellular network in POS settings.' +STRING_WOOPOS_REFRESH_CATALOG_BANNER_DISMISS='Dismiss banner' +STRING_WOOPOS_WC_VERSION_SUNSET_BANNER_TITLE='Update WooCommerce soon' +STRING_WOOPOS_WC_VERSION_SUNSET_BANNER_MESSAGE='Starting August 1, 2026, Point of Sale will require WooCommerce 10.5.0 or later. Update WooCommerce on your store or ask your store administrator.' +STRING_WOOPOS_WC_VERSION_SUNSET_BANNER_DISMISS='Dismiss WooCommerce update warning' +STRING_WOOPOS_READER_CONNECTED='Reader connected' +STRING_WOOPOS_READER_DISCONNECTED='Connect your reader' +STRING_WOOPOS_READER_RECONNECTING='Reconnecting…' +STRING_WOOPOS_REMOTE_READER_CONNECT_FAILED_GENERIC='Connection failed' +STRING_WOOPOS_BATTERY_LOW='Card reader battery low' +STRING_WOOPOS_BATTERY_CRITICAL='Card reader battery critical' +STRING_WOOPOS_CHECKOUT_BUTTON='Check out' +STRING_WOOPOS_REMOVE_ITEM_BUTTON_FROM_CART_CONTENT_DESCRIPTION_RE='Remove\ .*\ from\ cart' +STRING_WOOPOS_PRODUCT_ITEM_CONTENT_DESCRIPTION_RE='Product\ .*,\ Price\ .*' +STRING_WOOPOS_VARIABLE_PRODUCT_ITEM_CONTENT_DESCRIPTION_V2_RE='Variable\ Product\ .*' +STRING_WOOPOS_VARIATION_ITEM_CONTENT_DESCRIPTION_RE='Variation\ .*,\ Price\ .*' +STRING_WOOPOS_CART_ITEM_PRODUCT_CONTENT_DESCRIPTION_RE='Product\ in\ cart\ .*,\ Price\ .*' +STRING_WOOPOS_CART_CLEAR_ALL_BUTTON_CONTENT_DESCRIPTION='Clear items in cart icon' +STRING_WOOPOS_CART_ITEM_LOADING_CONTENT_DESCRIPTION_RE='Loading\ item\ .*' +STRING_WOOPOS_CART_ITEM_ERROR_CONTENT_DESCRIPTION_RE='Error\ loading\ item\ .*' +STRING_WOOPOS_COUPON_ITEM_CONTENT_DESCRIPTION_V2_RE='Coupon\ .*,\ .*' +STRING_WOOPOS_COUPON_ITEM_EXPIRED_LABEL_RE='Expired\ on\ .*' +STRING_WOOPOS_ADD_PRODUCT_TO_CART_ACCESSIBILITY_LABEL='Add product to cart' +STRING_WOOPOS_ADD_VARIABLE_PRODUCT_TO_CART_ACCESSIBILITY_LABEL='Open list of variations' +STRING_WOOPOS_ADD_COUPON_TO_CART_ACCESSIBILITY_LABEL='Add coupon to cart' +STRING_WOOPOS_CART_ITEM_COUPON_CONTENT_DESCRIPTION_RE='Coupon\ in\ cart\ .*' +STRING_WOOPOS_CART_ITEM_CUSTOM_AMOUNT_CONTENT_DESCRIPTION_RE='Custom\ amount\ .*,\ .*' +STRING_WOOPOS_CART_CUSTOM_AMOUNT_EDIT_CONTENT_DESCRIPTION_RE='Edit\ .*' +STRING_WOOPOS_CART_CUSTOM_AMOUNT_INCLUDES_TAX='Tax included' +STRING_WOOPOS_CART_CUSTOM_AMOUNT_DISCOUNT_NOT_APPLIED='Discount not applied' +STRING_WOOPOS_CART_CUSTOM_AMOUNT_MENU_EDIT='Edit' +STRING_WOOPOS_CART_CUSTOM_AMOUNT_MENU_REMOVE='Remove' +STRING_WOOPOS_CUSTOM_AMOUNT_ENTRY_TITLE='Custom amount' +STRING_WOOPOS_CUSTOM_AMOUNT_ENTRY_SUBTITLE='Add a one-off charge' +STRING_WOOPOS_CUSTOM_AMOUNT_DIALOG_TITLE_ADD='Add custom amount' +STRING_WOOPOS_CUSTOM_AMOUNT_DIALOG_TITLE_EDIT='Edit custom amount' +STRING_WOOPOS_CUSTOM_AMOUNT_FORM_TITLE='Custom amount' +STRING_WOOPOS_CUSTOM_AMOUNT_DIALOG_AMOUNT_LABEL='Amount' +STRING_WOOPOS_CUSTOM_AMOUNT_DIALOG_NAME_LABEL='Name' +STRING_WOOPOS_CUSTOM_AMOUNT_DIALOG_NAME_PLACEHOLDER='Custom amount' +STRING_WOOPOS_CUSTOM_AMOUNT_DIALOG_CHARGE_TAXES='Charge taxes' +STRING_WOOPOS_CUSTOM_AMOUNT_DIALOG_SUBMIT_ADD='Add custom amount' +STRING_WOOPOS_CUSTOM_AMOUNT_DIALOG_SUBMIT_EDIT='Update custom amount' +STRING_WOOPOS_CUSTOM_AMOUNT_DIALOG_CANCEL='Cancel' +STRING_WOOPOS_CART_TITLE='Cart' +STRING_WOOPOS_CART_CHANGES_IN_THE_CART='There are changes in the cart' +STRING_WOOPOS_CART_BARCODE_SCAN_RESULT_PRODUCT_NOT_FOUND='Unknown scanned item' +STRING_WOOPOS_CART_BARCODE_SCAN_RESULT_UNSUPPORTED_PRODUCT_RE='Unsupported\ item\ –\ .*' +STRING_WOOPOS_CART_BARCODE_SCAN_RESULT_SERVER_ERROR_RE='Server\ error\ \-\ .*' +STRING_WOOPOS_CART_BARCODE_SCAN_RESULT_NETWORK_ERROR='No internet connection' +STRING_WOOPOS_CART_BARCODE_SCAN_RESULT_TOO_SHORT='Scanned barcode is too short' +STRING_WOOPOS_CART_BARCODE_SCAN_RESULT_NO_TERMINATOR='Scanner did not send end-of-line character' +STRING_WOOPOS_CLEAR_CART_BUTTON='Clear cart' +STRING_WOOPOS_BANNER_SIMPLE_PRODUCTS_DIALOG_CONTENT_DESCRIPTION='Simple, variable and virtual products only dialog' +STRING_WOOPOS_BANNER_SIMPLE_PRODUCTS_DIALOG_PRIMARY_BUTTON_CONTENT_DESCRIPTION='Double tap to dismiss the dialog' +STRING_WOOPOS_DIALOG_PRODUCTS_INFO_HEADING='Why can'"'"'t I see my products?' +STRING_WOOPOS_DIALOG_PRODUCTS_INFO_PRIMARY_MESSAGE='Only simple physical, variable and virtual products can be used with POS right now.' +STRING_WOOPOS_DIALOG_PRODUCTS_INFO_SECONDARY_MESSAGE='Other product types will be available in future updates.' +STRING_WOOPOS_DIALOG_PRODUCTS_INFO_TERTIARY_MESSAGE='To take payment for other products, exit POS and create a new order from the orders tab.' +STRING_WOOPOS_DIALOG_PRODUCTS_INFO_BUTTON_LABEL='OK' +STRING_WOOPOS_DIALOG_PRODUCTS_INFO_BACKGROUND_CONTENT_DESCRIPTION='Dimmed background. Tap to dismiss the dialog.' +STRING_WOOPOS_SETTINGS_TITLE='Settings' +STRING_WOOPOS_SETTINGS_HARDWARE_CATEGORY='Hardware' +STRING_WOOPOS_SETTINGS_HARDWARE_CATEGORY_SUBTITLE='Manage hardware connections' +STRING_WOOPOS_SETTINGS_STORE_CATEGORY='Store' +STRING_WOOPOS_SETTINGS_STORE_CATEGORY_SUBTITLE='Store configuration and settings' +STRING_WOOPOS_SETTINGS_HARDWARE_BARCODE_SCANNERS='Barcode scanners' +STRING_WOOPOS_SETTINGS_HARDWARE_BARCODE_SCANNERS_SUBTITLE='Configure barcode scanner settings' +STRING_WOOPOS_SETTINGS_HARDWARE_CARD_READERS='Card Readers' +STRING_WOOPOS_SETTINGS_HARDWARE_CARD_READERS_SUBTITLE='Manage card reader connections' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_CATEGORY='Product catalog' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_CATEGORY_SUBTITLE='Manage catalog settings' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_STATUS='Catalog Status' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_SIZE='Size' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_LAST_UPDATE='Last incremental sync' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_LAST_FULL_UPDATE='Last full sync' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_SETTINGS='Manage Data Usage' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_CELLULAR_DATA='Cellular data' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_CELLULAR_DATA_SUBTITLE='Allow sync using cellular data' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_MANUAL_UPDATE='Catalog update' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_REFRESH_DESCRIPTION='Update the catalog manually' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_REFRESH_BUTTON='Update catalog' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_SIZE_FORMAT_RE='.*\ products\ and\ .*\ variations' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_SYNC_ERROR_DIALOG_BACKGROUND_CONTENT_DESCRIPTION='Sync error dialog background' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_SYNC_ERROR_DIALOG_TITLE='Unable to sync catalog' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_SYNC_ERROR_BLOCKED_TITLE='Action needed on your store' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_SYNC_ERROR_DIALOG_MESSAGE='Please check your internet connection and try again.' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_SYNC_ERROR_BLOCKED_MESSAGE='Point of Sale can'"'"'t access a file with your product information because your hosting provider is blocking it. Please ask them to allow access to it so Point of Sale keeps working properly.' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_SYNC_ERROR_DIALOG_RETRY_BUTTON='Retry' +STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_SYNC_ERROR_DIALOG_CANCEL_BUTTON='Cancel' +STRING_WOOPOS_DATE_NEVER='Never' +STRING_WOOPOS_DATE_JUST_NOW='Just now' +STRING_WOOPOS_DATE_TODAY_AT_RE='Today\ at\ .*' +STRING_WOOPOS_DATE_YESTERDAY_AT_RE='Yesterday\ at\ .*' +STRING_WOOPOS_SETTINGS_BARCODE_SCANNER_DETAIL_TITLE='Barcode Scanner Settings' +STRING_WOOPOS_SETTINGS_BARCODE_SCANNER_SETUP_TITLE='Scanner Setup' +STRING_WOOPOS_SETTINGS_BARCODE_SCANNER_SETUP_SUBTITLE='Configure and test your barcode scanner' +STRING_WOOPOS_SETTINGS_BARCODE_SCANNER_DOCUMENTATION_TITLE='Documentation' +STRING_WOOPOS_SETTINGS_BARCODE_SCANNER_DOCUMENTATION_SUBTITLE='Learn more about barcode scanning in POS' +STRING_WOOPOS_SETTINGS_CARD_READER_DETAIL_TITLE='Card Reader Settings' +STRING_WOOPOS_SETTINGS_CARD_READER_DEVICE_NAME_TITLE='Device name' +STRING_WOOPOS_SETTINGS_CARD_READER_BATTERY_TITLE='Battery' +STRING_WOOPOS_SETTINGS_CARD_READER_FIRMWARE_TITLE='Firmware' +STRING_WOOPOS_SETTINGS_CARD_READER_UNKNOWN_READER='Unknown Reader' +STRING_WOOPOS_SETTINGS_CARD_READER_UNKNOWN_FIRMWARE='Unknown' +STRING_WOOPOS_SETTINGS_CARD_READER_UPDATE_BUTTON='Update firmware' +STRING_WOOPOS_SETTINGS_CARD_READER_UPDATE_AVAILABLE='• Update available' +STRING_WOOPOS_SETTINGS_CARD_READER_DOCUMENTATION_TITLE='Documentation' +STRING_WOOPOS_SETTINGS_CARD_READER_DOCUMENTATION_SUBTITLE='Learn more about accepting mobile payments' +STRING_WOOPOS_SETTINGS_CARD_READER_CONNECTED_READER='Connected reader' +STRING_WOOPOS_SETTINGS_CARD_READER_DISCONNECT_BUTTON='Disconnect' +STRING_WOOPOS_SETTINGS_CARD_READER_UPDATE_FIRMWARE_TITLE='Update firmware version' +STRING_WOOPOS_SETTINGS_CARD_READER_UPDATE_FIRMWARE_MESSAGE='Update the firmware version to continue accepting payments.' +STRING_WOOPOS_SETTINGS_CARD_READER_TRANSPORT_TITLE='Transport' +STRING_WOOPOS_SETTINGS_CARD_READER_TRANSPORT_WIFI='Wi-Fi' +STRING_WOOPOS_SETTINGS_CARD_READER_FINGERPRINT_TITLE='Fingerprint' +STRING_WOOPOS_SETTINGS_HELP_CATEGORY='Get help and support' +STRING_WOOPOS_SETTINGS_HELP_PRODUCT_LIMITATIONS_SUBTITLE='Learn about which products are supported in POS' +STRING_WOOPOS_SETTINGS_HELP_DOCUMENTATION_SUBTITLE='View guides and tutorials' +STRING_WOOPOS_SETTINGS_HELP_GET_SUPPORT_SUBTITLE='Contact our support team' +STRING_WOOPOS_EXIT_CONFIRMATION_TITLE='Exit POS' +STRING_WOOPOS_GET_SUPPORT_TITLE='Get Support' +STRING_WOOPOS_PRODUCT_LIMITATIONS_TITLE='Where are my products?' +STRING_WOOPOS_DOCUMENTATION_TITLE='Documentation' +STRING_WOOPOS_ORDERS_TITLE='Orders' +STRING_WOOPOS_ORDER_TITLE_RE='Order\ .*' +STRING_WOOPOS_EXIT_DIALOG_CONFIRMATION_CLOSE_CONTENT_DESCRIPTION='Close' +STRING_WOOPOS_EXIT_DIALOG_CONFIRMATION_TITLE='Exit Point of Sale mode?' +STRING_WOOPOS_EXIT_DIALOG_CONFIRMATION_MESSAGE='Any orders in progress will be lost.' +STRING_WOOPOS_EXIT_DIALOG_CONFIRMATION_CONFIRM_BUTTON='Exit' +STRING_WOOPOS_DIALOG_EXIT_CONFIRMATION_BACKGROUND_CONTENT_DESCRIPTION='Dimmed background. Tap to dismiss the dialog.' +STRING_WOOPOS_MENU_TOOLBAR_CONTENT_DESCRIPTION='Menu' +STRING_WOOPOS_ITEMS_IN_CART_RE='.*\ item' +STRING_WOOPOS_ITEMS_IN_CART_MULTIPLE_RE='.*\ items' +STRING_WOOPOS_REMOVE_CART_ITEM_CONTENT_DESCRIPTION='Remove this item from the cart' +STRING_WOOPOS_PRODUCTS_SCREEN_TITLE='Products' +STRING_WOOPOS_COUPONS_SCREEN_TITLE='Coupons' +STRING_WOOPOS_CART_BACK_CONTENT_DESCRIPTION='Cart icon' +STRING_WOOPOS_CART_COUPON_INVALID_SUBTITLE='Coupon not applied' +STRING_WOOPOS_PAYMENT_SUCCESSFUL_LABEL='Payment successful' +STRING_WOOPOS_NEW_ORDER_BUTTON='New order' +STRING_WOOPOS_RECEIPT_BUTTON='Email receipt' +STRING_WOOPOS_RECEIPT_SENT_TO_CUSTOMER_RE='A\ receipt\ has\ been\ sent\ to\ .*\.' +STRING_WOOPOS_PAYMENT_DISCOUNT_LABEL='Discount total' +STRING_WOOPOS_PAYMENT_SUBTOTAL_LABEL='Subtotal' +STRING_WOOPOS_PAYMENT_TAX_LABEL='Taxes' +STRING_WOOPOS_PAYMENT_TOTAL_LABEL='Total' +STRING_WOOPOS_PAYMENT_TAKE_CASH_PAYMENT_LABEL='Cash payment' +STRING_WOOPOS_PAYMENT_METHOD_CARD_READER_LABEL='Card reader' +STRING_WOOPOS_PAYMENT_METHOD_TAP_TO_PAY_LABEL='Tap to Pay' +STRING_WOOPOS_PAYMENT_METHOD_ALL_METHODS_LABEL='All payment methods' +STRING_WOOPOS_PAYMENT_METHOD_OTHER_METHODS_LABEL='Other payment methods' +STRING_WOOPOS_PAYMENT_METHOD_SCAN_TO_PAY_LABEL='Scan to pay' +STRING_WOOPOS_PAYMENT_METHOD_MARK_ORDER_AS_PAID_LABEL='Mark order as paid' +STRING_WOOPOS_MARK_ORDER_AS_PAID_TITLE='Mark order as paid?' +STRING_WOOPOS_MARK_ORDER_AS_PAID_MESSAGE_RE='This\ will\ mark\ the\ .*\ order\ as\ completed\.\ Use\ this\ only\ if\ you'"'"'ve\ already\ collected\ payment\ another\ way\.' +STRING_WOOPOS_MARK_ORDER_AS_PAID_NOTE_HINT='e.g. Bank transfer from Maria, ref 4827' +STRING_WOOPOS_MARK_ORDER_AS_PAID_CONFIRM_BUTTON='Mark as paid' +STRING_WOOPOS_MARK_ORDER_AS_PAID_ERROR_MESSAGE='Couldn'"'"'t update the order. Try again.' +STRING_WOOPOS_MARK_ORDER_AS_PAID_ORDER_NOT_FOUND='Order could not be loaded. Go back and try again.' +STRING_WOOPOS_SCAN_TO_PAY_TITLE='Scan to pay' +STRING_WOOPOS_SCAN_TO_PAY_SUBTITLE='Show this code to the customer to let them pay from their phone.' +STRING_WOOPOS_SCAN_TO_PAY_TOTAL_RE='Order\ total:\ .*' +STRING_WOOPOS_SCAN_TO_PAY_ERROR_MESSAGE='We couldn'"'"'t prepare the payment. Please try again.' +STRING_WOOPOS_SCAN_TO_PAY_RETRY='Try again' +STRING_WOOPOS_SCAN_TO_PAY_CANCEL='Cancel' +STRING_WOOPOS_SCAN_TO_PAY_ORDER_NOTE='Customer paid via Scan to Pay' +STRING_WOOPOS_PAYMENT_METHOD_PICKER_BOTTOM_SHEET_TITLE='Choose payment method' +STRING_WOOPOS_PAYMENT_METHOD_PICKER_BOTTOM_SHEET_SCRIM_CONTENT_DESCRIPTION='Dimmed background. Tap to dismiss the payment method picker.' +STRING_WOOPOS_TAP_TO_PAY_PROMOTED_TITLE='Tap to pay' +STRING_WOOPOS_TAP_TO_PAY_PROMOTED_SUBTITLE='Use this device to accept contactless card payments.' +STRING_WOOPOS_TAP_TO_PAY_PROMOTED_CTA_BUTTON_LABEL='Pay with Tap to pay' +STRING_WOOPOS_TAP_TO_PAY_PROMOTED_IMAGE_DESCRIPTION='Tap to Pay on phone illustration' +STRING_WOOPOS_TAP_TO_PAY_PREPARING_TITLE='Getting ready' +STRING_WOOPOS_TAP_TO_PAY_PREPARING_SUBTITLE='Preparing Tap to Pay' +STRING_WOOPOS_TAP_TO_PAY_PAYMENT_FAILED_MESSAGE='Tap to Pay couldn'"'"'t start. Please try again.' +STRING_WOOPOS_TAP_TO_PAY_PAYMENT_FAILED_WITH_REASON_MESSAGE_RE='Tap\ to\ Pay\ couldn'"'"'t\ start:\ .*' +STRING_WOOPOS_TAP_TO_PAY_MISSING_LOCATION_PERMISSION_MESSAGE='Grant location permission to use Tap to Pay.' +STRING_WOOPOS_TOTALS_MAIN_ERROR_LABEL='Couldn'"'"'t load totals' +STRING_WOOPOS_TOTALS_COUPONS_VALIDATION_FAILED_EDIT_ORDER='Edit order' +STRING_WOOPOS_TOTALS_COUPONS_VALIDATION_FAILED_REMOVE_COUPONS='Remove coupons' +STRING_WOOPOS_CART_EMPTY_SUBTITLE_WITH_SCANNER='Tap on a product or' +STRING_WOOPOS_CART_EMPTY_SCAN_BARCODE='scan a barcode' +STRING_WOOPOS_CART_EMPTY_SUBTITLE_SUFFIX='to add it to the cart' +STRING_WOOPOS_CART_EMPTY_CONTENT_DESCRIPTION='Cart is empty' +STRING_WOOPOS_PRODUCTS_EMPTY_LIST_IMAGE_DESCRIPTION='No products' +STRING_WOOPOS_PRODUCTS_EMPTY_LIST_TITLE='No supported products found' +STRING_WOOPOS_PRODUCTS_EMPTY_LIST_MESSAGE='POS currently only supports simple, variable, and virtual products – \ncreate one to get started.' +STRING_WOOPOS_PRODUCTS_LOADING_ERROR_TITLE='Unable to load products' +STRING_WOOPOS_PRODUCTS_LOADING_ERROR_MESSAGE='Please try again.' +STRING_WOOPOS_COUPONS_EMPTY_LIST_IMAGE_DESCRIPTION='No coupons' +STRING_WOOPOS_COUPONS_EMPTY_LIST_TITLE='No coupons found' +STRING_WOOPOS_COUPONS_LOADING_ERROR_COUPONS_DISABLED_TITLE='Start accepting coupons' +STRING_WOOPOS_COUPONS_LOADING_ERROR_COUPONS_DISABLED_MESSAGE='Enable coupon codes in WooCommerce settings to start creating them for your customers.' +STRING_WOOPOS_COUPONS_EMPTY_LIST_MESSAGE='Coupons can be effective way to drive business. Would you like to create one?' +STRING_WOOPOS_COUPONS_EMPTY_LIST_CREATE_COUPON_LABEL='Create coupon' +STRING_WOOPOS_PHONE_ITEMS_ADD_COUPON_FAB_ACCESSIBILITY_LABEL='Add coupon' +STRING_WOOPOS_COUPONS_LOADING_ERROR_TITLE='Unable to load coupons' +STRING_WOOPOS_COUPONS_LOADING_ERROR_MESSAGE='Please check your internet connection and try again.' +STRING_WOOPOS_ERROR_ICON_CONTENT_DESCRIPTION='Error indication icon' +STRING_WOOPOS_READER_NOT_CONNECTED_DESCRIPTION='Please make sure a card reader is connected.' +STRING_WOOPOS_PRODUCTS_LOADING_ERROR_RETRY_BUTTON='Retry' +STRING_WOOPOS_TOTALS_ORDER_CREATION_ERROR='Couldn'"'"'t create order' +STRING_WOOPOS_TOTALS_INVALID_COUPON_ERROR='Unable to apply coupons' +STRING_WOOPOS_TOTALS_PRODUCT_NOT_FOUND_ERROR='Product not found' +STRING_WOOPOS_TOTALS_PRODUCT_NOT_FOUND_REASON='A product in the cart is no longer available.' +STRING_WOOPOS_TOTALS_PRODUCT_NOT_FOUND_EDIT_ORDER='Edit order' +STRING_WOOPOS_TOTALS_PRODUCT_NOT_FOUND_REMOVE_PRODUCTS='Remove product' +STRING_WOOPOS_CART_PRODUCT_UNKNOWN_ITEM='Unknown item' +STRING_WOOPOS_TOTALS_SUCCESS_PAYMENT_CASH_RE='A\ cash\ payment\ of\ .*\ was\ successfully\ made\.' +STRING_WOOPOS_TOTALS_SUCCESS_PAYMENT_CARD_RE='A\ card\ payment\ of\ .*\ was\ successfully\ made\.' +STRING_WOOPOS_TOTALS_SUCCESS_PAYMENT_QR_RE='A\ payment\ of\ .*\ was\ successfully\ received\.' +STRING_WOOPOS_TOTALS_SUCCESS_PAYMENT_EXTERNAL_RE='Order\ of\ .*\ was\ marked\ as\ complete\.' +STRING_WOOPOS_VARIATIONS_EMPTY_LIST_TITLE='No supported variations found' +STRING_WOOPOS_VARIATIONS_EMPTY_LIST_MESSAGE='POS currently only supports simple, variable, and virtual products – \ncreate one to get started.' +STRING_WOOPOS_VARIATIONS_EMPTY_LIST_IMAGE_DESCRIPTION='No variations' +STRING_WOOPOS_VARIATIONS_ANY_VARIATION_RE='Any\ .*' +STRING_WOOPOS_SUCCESS_TOTALS_ERROR_READER_NOT_CONNECTED_TITLE='Reader not connected' +STRING_WOOPOS_SUCCESS_TOTALS_ERROR_READER_NOT_CONNECTED_SUBTITLE='To process this payment, please connect your reader.' +STRING_WOOPOS_SUCCESS_TOTALS_ERROR_READER_NOT_CONNECTED_CTA_BUTTON_LABEL='Connect to reader' +STRING_WOOPOS_TOTALS_READER_GETTING_READY='Getting ready' +STRING_WOOPOS_TOTALS_READER_CHECKING_ORDER='Checking order' +STRING_WOOPOS_TOTALS_READER_PREPARING_READER_FOR_PAYMENT='Preparing reader for payment' +STRING_WOOPOS_TOTALS_READER_READY_FOR_PAYMENT_TITLE='Ready for payment' +STRING_WOOPOS_TOTALS_READER_READY_FOR_PAYMENT_SUBTITLE='Tap, swipe or insert card' +STRING_WOOPOS_SUCCESS_TOTALS_PAYMENT_PROCESSING_TITLE='Processing payment' +STRING_WOOPOS_SUCCESS_TOTALS_PAYMENT_PROCESSING_SUBTITLE='Please wait…' +STRING_WOOPOS_SUCCESS_TOTALS_PAYMENT_FAILED_TITLE='Payment failed' +STRING_WOO_POS_PAYMENT_FAILED_TRY_ANOTHER_PAYMENT_METHOD='Try another payment method' +STRING_WOO_POS_PAYMENT_FAILED_TRY_AGAIN='Try payment again' +STRING_WOO_POS_PAYMENT_FAILED_GO_BACK_TO_CHECKOUT='Go back to checkout' +STRING_WOO_POS_PAYMENT_FAILED_GO_BACK='Go back' +STRING_WOOPOS_REMOTE_PAYMENT_FAILED_CONNECTION_LOST='Connection to the phone reader was lost. Please reconnect and try again.' +STRING_WOOPOS_REMOTE_PAYMENT_FAILED_GENERIC='Something went wrong. Please try again.' +STRING_WOOPOS_CARD_PAYMENT_DONE_BUTTON='Done' +STRING_WOOPOS_FLOATING_TOOLBAR_OVERLAY_MENU_CONTENT_DESCRIPTION='Dimmed background. Tap to close the menu.' +STRING_WOOPOS_FLOATING_TOOLBAR_CARD_READER_CONNECTED_STATUS_CONTENT_DESCRIPTION='Card reader connected' +STRING_WOOPOS_FLOATING_TOOLBAR_CARD_READER_NOT_CONNECTED_STATUS_CONTENT_DESCRIPTION='Card reader not connected. Double tap to connect' +STRING_WOOPOS_FLOATING_TOOLBAR_MENU_ENABLED_CONTENT_DESCRIPTION='Menu Enabled' +STRING_WOOPOS_FLOATING_TOOLBAR_MENU_DISABLED_CONTENT_DESCRIPTION='Menu Disabled' +STRING_WOOPOS_FLOATING_TOOLBAR_POP_UP_MENU_CONTENT_DESCRIPTION='Open toolbar menu' +STRING_WOOPOS_FLOATING_TOOLBAR_POP_UP_MENU_OPEN_CONTENT_DESCRIPTION='Popup menu with options. Swipe to navigate through items.' +STRING_WOOPOS_NO_INTERNET_MESSAGE='It looks like you'"'"'re not connected to the internet. Ensure your Wi-Fi is turned on. If you'"'"'re using mobile data, make sure it'"'"'s enabled in your device settings.' +STRING_WOOPOS_VARIATIONS_BACK_CONTENT_DESCRIPTION='Back' +STRING_WOOPOS_ITEMS_PAGINATION_TRY_AGAIN_LABEL='Retry' +STRING_WOOPOS_ITEMS_PAGINATION_ERROR_TITLE='Unable to load more products' +STRING_WOOPOS_ITEMS_PAGINATION_ERROR_DESCRIPTION='Please check your internet connection and try again.' +STRING_WOOPOS_ITEMS_PAGINATION_ERROR_CONTENT_DESCRIPTION='"Failed to load more items. Double tap to try again."' +STRING_WOOPOS_VARIATIONS_LOADING_ERROR_TITLE='Unable to load variations' +STRING_WOOPOS_VARIATIONS_LOADING_ERROR_MESSAGE='Please check your internet connection and try again.' +STRING_WOOPOS_VARIATIONS_OPTIONS_AVAILABLE_TEXT='Options available' +STRING_WOOPOS_COUPONS_PAGINATION_TRY_AGAIN_LABEL='Retry' +STRING_WOOPOS_COUPONS_PAGINATION_ERROR_TITLE='Unable to load more coupons' +STRING_WOOPOS_COUPONS_PAGINATION_ERROR_DESCRIPTION='Please check your internet connection and try again.' +STRING_WOOPOS_ORDERS_EMPTY_LIST_TITLE='No orders found' +STRING_WOOPOS_ORDERS_EMPTY_LIST_MESSAGE='Orders will appear here once you start processing sales on the POS.' +STRING_WOOPOS_ORDERS_EMPTY_ACTION_LABEL='Refresh' +STRING_WOOPOS_ORDERS_EMPTY_LIST_IMAGE_DESCRIPTION='No orders' +STRING_WOOPOS_ORDERS_NO_ORDER_SELECTED='No order selected.' +STRING_WOOPOS_ORDERS_LOADING_ERROR_TITLE='Unable to load orders' +STRING_WOOPOS_ORDERS_LOADING_ERROR_MESSAGE='Please check your connection and try again.' +STRING_WOOPOS_ORDERS_LOADING_ERROR_RETRY_BUTTON='Retry' +STRING_WOOPOS_ORDERS_PAGINATION_ERROR_TITLE='Unable to load more orders' +STRING_WOOPOS_ORDERS_PAGINATION_ERROR_CONTENT_DESCRIPTION='Please try again.' +STRING_WOOPOS_ORDERS_PAGINATION_TRY_AGAIN_LABEL='Retry' +STRING_WOOPOS_ORDERS_EMAIL_RECEIPT='Email receipt' +STRING_WOOPOS_ORDERS_ISSUE_REFUND_CONTENT_DESCRIPTION='Issue a refund for this order' +STRING_WOOPOS_ORDERS_SELECT_ITEMS_TO_REFUND='Select items to refund' +STRING_WOOPOS_ORDERS_SELECT_ALL_ITEMS='SELECT ALL ITEMS' +STRING_WOOPOS_ORDERS_ITEMS_SELECTED_COUNT_RE='\(.*\ SELECTED\)' +STRING_WOOPOS_ORDERS_LOADING_REFUND_ITEMS='Loading refundable items' +STRING_WOOPOS_ORDERS_NO_ITEMS_AVAILABLE_FOR_REFUND='No items available for refund' +STRING_WOOPOS_ORDERS_REVIEW_REFUND='Review refund' +STRING_WOOPOS_ORDERS_ISSUE_REFUND='Issue Refund' +STRING_WOOPOS_ORDERS_REFUND_TOTAL='Refund total' +STRING_WOOPOS_ORDERS_VIA_PAYMENT_METHOD_RE='Via\ .*' +STRING_WOOPOS_ORDERS_REFUND_REASON='Refund reason' +STRING_WOOPOS_ORDERS_REFUND_REASON_PLACEHOLDER='Reason for refunding order' +STRING_WOOPOS_ORDERS_EDIT_REASON='Edit reason' +STRING_WOOPOS_ORDERS_REFUND_REASON_ADD='Add' +STRING_WOOPOS_ORDERS_REFUND_REASON_SAVE='Save' +STRING_WOOPOS_ORDERS_EDIT_REFUND='Edit refund' +STRING_WOOPOS_ORDERS_CONFIRM_REFUND_TITLE_RE='Refund\ .*' +STRING_WOOPOS_ORDERS_CONFIRM_REFUND_MESSAGE_RE='Are\ you\ sure\ you\ wish\ to\ process\ the\ refund\ .*\ via\ .*\?\\n\\nThis\ action\ cannot\ be\ undone\.' +STRING_WOOPOS_ORDERS_YES_PROCEED='Yes, proceed' +STRING_WOOPOS_REFUND_DISCARD_CHANGES_TITLE='Discard refund changes?' +STRING_WOOPOS_REFUND_DISCARD_CHANGES_MESSAGE='Changing orders will discard the refund changes you made.' +STRING_WOOPOS_REFUND_DISCARD_CHANGES_DISCARD='Discard changes' +STRING_WOOPOS_REFUND_DISCARD_CHANGES_DIALOG_BACKGROUND_CONTENT_DESCRIPTION='Discard refund changes dialog background' +STRING_WOOPOS_REFUND_LOADING_ERROR_TITLE='Failed to load refund items' +STRING_WOOPOS_REFUND_CREATING_ERROR_TITLE='Failed to create refund' +STRING_WOOPOS_REFUND_ERROR_SUBTITLE='Please try again' +STRING_WOOPOS_REFUND_READER_TITLE='Refund card payment' +STRING_WOOPOS_REFUND_READER_PREPARING_TITLE='Preparing reader for refund' +STRING_WOOPOS_REFUND_READER_PREPARING_SUBTITLE='Getting reader ready' +STRING_WOOPOS_REFUND_READER_NOT_CONNECTED_TITLE='Reader not connected' +STRING_WOOPOS_REFUND_READER_NOT_CONNECTED_SUBTITLE='To process this refund, please connect your reader.' +STRING_WOOPOS_REFUND_READER_NOT_CONNECTED_CTA_BUTTON_LABEL='Connect to reader' +STRING_WOOPOS_REFUND_READER_READY_TITLE='Ready for refund' +STRING_WOOPOS_REFUND_READER_READY_SUBTITLE='Tap, insert, or swipe card to refund' +STRING_WOOPOS_REFUND_READER_PROCESSING_TITLE='Processing refund' +STRING_WOOPOS_REFUND_NOTIFYING_STORE_TITLE='Notifying store' +STRING_WOOPOS_REFUND_NOTIFYING_STORE_SUBTITLE='Applying refund to order' +STRING_WOOPOS_REFUND_BACK_TO_ORDER_BUTTON='Back to order' +STRING_WOOPOS_ORDERS_REFUND_COMPLETE='Refund complete' +STRING_WOOPOS_ORDERS_REFUND_SUCCESS_MESSAGE_RE='You\ refunded\ .*\ via\ .*\.' +STRING_WOOPOS_ORDERS_DETAILS_REFRESH_ERROR='Couldn'"'"'t refresh the order. Please check your connection and try again.' +STRING_WOOPOS_ORDERS_DETAILS_PLACEHOLDER_TITLE='Select an order' +STRING_WOOPOS_ORDERS_DETAILS_PLACEHOLDER_MESSAGE='Choose an order from the list to view details.' +STRING_WOOPOS_ORDERS_DETAILS_PRODUCTS_TITLE='Products' +STRING_WOOPOS_ORDERS_DETAILS_REFUNDED_PRODUCTS_TITLE='Refunded Products' +STRING_WOOPOS_ORDERS_DETAILS_TOTALS_TITLE='Totals' +STRING_WOOPOS_ORDERS_DETAILS_QTY_UNIT_PRICE_FORMAT_RE='.*\ x\ .*' +STRING_WOOPOS_ORDERS_DETAILS_BREAKDOWN_PRODUCTS_LABEL='Products' +STRING_WOOPOS_ORDERS_DETAILS_BREAKDOWN_DISCOUNT_LABEL='Discount total' +STRING_WOOPOS_ORDERS_DETAILS_BREAKDOWN_DISCOUNT_WITH_CODE_LABEL_RE='Discount\ total\ \(.*\)' +STRING_WOOPOS_ORDERS_DETAILS_BREAKDOWN_TAXES_LABEL='Taxes' +STRING_WOOPOS_ORDERS_DETAILS_BREAKDOWN_SHIPPING_LABEL='Shipping' +STRING_WOOPOS_ORDERS_DETAILS_TOTAL_LABEL='Total' +STRING_WOOPOS_ORDERS_DETAILS_TOTAL_PAID_LABEL='Total paid' +STRING_WOOPOS_ORDERS_DETAILS_REFUNDED_LABEL='Refunded' +STRING_WOOPOS_ORDERS_DETAILS_REFUND_ERROR='Unable to load refund' +STRING_WOOPOS_REFUND_ERROR_GATEWAY_NOT_FOUND='Unable to process refund.' +STRING_WOOPOS_REFUND_PREVIEW_ERROR='Couldn'"'"'t calculate the refund total. Please try again.' +STRING_WOOPOS_ORDERS_DETAILS_NET_PAYMENT_LABEL='Total Net' +STRING_WOOPOS_ORDERS_DETAILS_PRODUCTS_LIST_CONTENT_DESCRIPTION='Order products list' +STRING_WOOPOS_ORDERS_DETAILS_TOTALS_CARD_CONTENT_DESCRIPTION='Order totals breakdown' +STRING_WOOPOS_ORDERS_DETAILS_REFUND_LABEL_NUMBERED_RE='Refund\ \#.*' +STRING_WOOPOS_ORDERS_DETAILS_REFUND_VIEW_DETAILS='View details' +STRING_WOOPOS_ORDERS_DETAILS_REFUND_ITEMS_SUBTOTAL_ONE_RE='Items\ subtotal\ \(.*\ item\)' +STRING_WOOPOS_ORDERS_DETAILS_REFUND_ITEMS_SUBTOTAL_OTHER_RE='Items\ subtotal\ \(.*\ items\)' +STRING_WOOPOS_ORDERS_DETAILS_REFUND_TAX='Tax' +STRING_WOOPOS_ORDERS_DETAILS_REFUND_TOTAL='Refund total' +STRING_WOOPOS_ORDERS_DETAILS_REFUND_DETAILS_BACKGROUND='Refund details dialog background' +STRING_WOOPOS_ORDERS_STATUS_AUTO_DRAFT='Draft' +STRING_WOOPOS_ORDERS_STATUS_PENDING='Pending Payment' +STRING_WOOPOS_ORDERS_STATUS_PROCESSING='Processing' +STRING_WOOPOS_ORDERS_STATUS_ON_HOLD='On hold' +STRING_WOOPOS_ORDERS_STATUS_FAILED='Failed' +STRING_WOOPOS_ORDERS_STATUS_CANCELLED='Canceled' +STRING_WOOPOS_ORDERS_STATUS_COMPLETED='Completed' +STRING_WOOPOS_ORDERS_STATUS_REFUNDED='Refunded' +STRING_WOOPOS_CASH_PAYMENT_TITLE='Cash payment' +STRING_WOOPOS_COMPLETE_CASH_ORDER_BUTTON='Mark payment as complete' +STRING_WOOPOS_CASH_PAYMENT_TOTAL_RE='Total:\ .*' +STRING_WOOPOS_CASH_PAYMENT_CHANGE_DUE_RE='Change\ due\ .*' +STRING_WOOPOS_CASH_PAYMENT_ERROR_MESSAGE='Error trying to process payment. Try again.' +STRING_WOOPOS_EMAIL_RECEIPT_SEND_BUTTON='Send' +STRING_WOOPOS_TOOLBAR_ICON_CONTENT_DESCRIPTION='Back' +STRING_WOOPOS_EMAIL_RECEIPT_TITLE='Email receipt' +STRING_WOOPOS_EMAIL_RECEIPT_EMAIL_LABEL='Type email' +STRING_WOOPOS_EMAIL_RECEIPT_SEND_ERROR='Error trying to send this email. Try again' +STRING_WOOPOS_SEARCH_BACK_CONTENT_DESCRIPTION='Back' +STRING_WOOPOS_SEARCH_CLEAR_CONTENT_DESCRIPTION='Clear' +STRING_WOOPOS_SEARCH_PRODUCTS='Search products' +STRING_WOOPOS_SEARCH_PRODUCTS_AND_VARIATIONS='Search products and variations' +STRING_WOOPOS_SEARCH_COUPONS='Search coupons' +STRING_WOOPOS_SEARCH_ORDERS='Search orders' +STRING_WOOPOS_SEARCH_POPULAR_ITEMS_TITLE='Popular products' +STRING_WOOPOS_SEARCH_RECENT_SEARCHES_TITLE='Recent searches' +STRING_WOOPOS_SEARCH_ITEMS_EMPTY_TITLE='No products found' +STRING_WOOPOS_SEARCH_COUPONS_EMPTY_TITLE='No coupons found' +STRING_WOOPOS_SEARCH_EMPTY_DESCRIPTION='We couldn'"'"'t find any matching products. Try adjusting your search term.' +STRING_WOOPOS_SEARCH_EMPTY_COUPONS_DESCRIPTION='We couldn'"'"'t find any matching coupons. Try adjusting your search term.' +STRING_WOOPOS_SEARCH_ORDERS_EMPTY_TITLE='No orders found' +STRING_WOOPOS_SEARCH_ORDERS_EMPTY_DESCRIPTION='We couldn'"'"'t find any orders with that name. Try adjusting your search term.' +STRING_WOOPOS_SEARCH_EMPTY_IMAGE_CONTENT_DESCRIPTION='No items found' +STRING_WOOPOS_SEARCH_ITEMS_ERROR_TITLE='Unable to load products' +STRING_WOOPOS_SEARCH_COUPONS_ERROR_TITLE='Unable to load coupons' +STRING_WOOPOS_SEARCH_ORDERS_ERROR_TITLE='Unable to load orders' +STRING_WOOPOS_SEARCH_ITEMS_ERROR_DESCRIPTION='Please try again.' +STRING_WOOPOS_SEARCH_ORDERS_ERROR_DESCRIPTION='Please try again.' +STRING_WOOPOS_ELIGIBILITY_EXIT_POS_LABEL='Exit POS' +STRING_WOOPOS_ELIGIBILITY_RETRY_CHECK_LABEL='Retry' +STRING_WOOPOS_ELIGIBILITY_REASON_UNSUPPORTED_WOOCOMMERCE_VERSION_RE='Your\ WooCommerce\ version\ is\ not\ supported\.\ The\ POS\ system\ requires\ WooCommerce\ version\ .*\ or\ above\.\ Please\ update\ WooCommerce\ to\ the\ latest\ version\.' +STRING_WOOPOS_ELIGIBILITY_REASON_CHECK_CONNECTION='Check your internet connection and try relaunching the app. If the issue persists, please contact support.' +STRING_WOOPOS_ELIGIBILITY_SCREEN_UNABLE_TO_LOAD='Unable to load' +STRING_WOOPOS_SCANNING_SETUP_DIALOG_CONTENT_DESCRIPTION='Scanner setup dialog' +STRING_WOOPOS_SCANNING_SETUP_BARCODE_CONTENT_DESCRIPTION='Barcode' +STRING_WOOPOS_SCANNING_SETUP_DEVICE_SELECTION_TITLE='Set up a barcode scanner' +STRING_WOOPOS_SCANNING_SETUP_DEVICE_SELECTION_MESSAGE='Select a model from the list:' +STRING_WOOPOS_SCANNING_SETUP_HID_TITLE='Scanner setup' +STRING_WOOPOS_SCANNING_SETUP_HID_MESSAGE='Use your barcode scanner to scan the code below to enable Bluetooth HID mode.' +STRING_WOOPOS_SCANNING_SETUP_SCANNER_PAIR_MODE_TITLE='Scanner setup' +STRING_WOOPOS_SCANNING_SETUP_SCANNER_PAIR_MODE_MESSAGE='Use your barcode scanner to scan the code below to enter pairing mode.' +STRING_WOOPOS_SCANNING_SETUP_PAIR_YOUR_SCANNER_TITLE='Pair your scanner' +STRING_WOOPOS_SCANNING_SETUP_PAIR_YOUR_SCANNER_MESSAGE_RE='Enable\ Bluetooth\ and\ select\ your\ .*\ scanner\ in\ the\ OS\ settings\.\\nThe\ scanner\ will\ beep\ and\ show\ a\ solid\ LED\ when\ paired\.' +STRING_WOOPOS_SCANNING_SETUP_GO_TO_SETTINGS='Go to your device settings' +STRING_WOOPOS_SCANNING_SETUP_SUCCESS_TITLE='Scanner set up!' +STRING_WOOPOS_SCANNING_SETUP_SUCCESS_MESSAGE='You are ready to start scanning products. Next time you need to connect your scanner, just turn on the scanner and it'"'"'ll reconnect automatically.' +STRING_WOOPOS_SCANNING_SETUP_MORE_INFORMATION='How to set up barcodes on products' +STRING_WOOPOS_SCANNING_SETUP_INFO_TITLE='Scanner setup' +STRING_WOOPOS_SCANNING_SETUP_INFO_MESSAGE='You can scan barcodes using an external scanner to quickly build a cart.' +STRING_WOOPOS_SCANNING_SETUP_INFO_BULLET_1='Refer to your bluetooth barcode scanner in System Bluetooth settings.' +STRING_WOOPOS_SCANNING_SETUP_INFO_BULLET_2='Scan barcodes while on the item list to add products to the cart.' +STRING_WOOPOS_SCANNING_SETUP_INFO_BULLET_3='Ensure the search field is not enabled while scanning barcodes.' +STRING_WOOPOS_SCANNING_SETUP_INFO_TEXT='Barcode scanner may hide keyboard. Tap keyboard icon or enable in OS settings under "Physical Keyboard"' +STRING_WOOPOS_SCANNING_SETUP_BARCODES_ON_PRODUCTS_TITLE='How to set up barcodes on products' +STRING_WOOPOS_SCANNING_SETUP_BARCODES_ON_PRODUCTS_MESSAGE='You can set up barcodes in the GTIN, UPC, EAN, ISBN field in the product'"'"'s inventory tab. For more details' +STRING_WOOPOS_SCANNING_SETUP_VISIT_DOCUMENTATION='visit the documentation' +STRING_WOOPOS_SCANNING_SETUP_BARCODES_ON_PRODUCTS_IMAGE_DESCRIPTION='Barcode setup inventory example' +STRING_WOOPOS_SCANNING_SETUP_TEST_SCANNER_TITLE='Test your scanner' +STRING_WOOPOS_SCANNING_SETUP_TEST_SCANNER_MESSAGE='Scan this test barcode to verify your scanner is working correctly.' +STRING_WOOPOS_SCANNING_SETUP_TIMEOUT_TITLE='No scan data found yet' +STRING_WOOPOS_SCANNING_SETUP_TIMEOUT_MESSAGE='Scan the barcode to test your scanner. If the issue continues, please check Bluetooth settings and try again.' +STRING_WOOPOS_SCANNING_SETUP_SCAN_FAILED_TITLE='Scanning issue found' +STRING_WOOPOS_SCANNING_SETUP_SCAN_FAILED_MESSAGE='Please check the scanner’s manual and reset it to factory settings, then retry the setup flow.' +STRING_WOOPOS_SCANNING_SETUP_BUTTON_NEXT='Next' +STRING_WOOPOS_SCANNING_SETUP_BUTTON_BACK='Back' +STRING_WOOPOS_SCANNING_SETUP_BUTTON_RETRY='Retry' +STRING_WOOPOS_SCANNING_SETUP_BUTTON_DONE='Done' +STRING_WOOPOS_SCANNING_SETUP_SOFTWARE_KEYBOARD_TITLE='Show the On-Screen Keyboard' +STRING_WOOPOS_SCANNING_SETUP_SOFTWARE_KEYBOARD_MESSAGE='Android treats barcode scanners as physical keyboards, which may hide your on-screen keyboard. Let'"'"'s check:' +STRING_WOOPOS_SCANNING_SETUP_SOFTWARE_KEYBOARD_MESSAGE_TWO='If the keyboard is not visible, try these options:' +STRING_WOOPOS_SCANNING_SETUP_SOFTWARE_KEYBOARD_HINT='Tap here to test keyboard visibility' +STRING_WOOPOS_SCANNING_SETUP_SOFTWARE_KEYBOARD_BULLET_ONE='Look for a keyboard icon or toolbar on the screen.' +STRING_WOOPOS_SCANNING_SETUP_SOFTWARE_KEYBOARD_BULLET_TWO='Go to device settings, search "keyboard", then enable "Show on-screen keyboard when physical keyboard connected". This setting may be in your keyboard app settings.' +STRING_WOOPOS_SCANNING_SETUP_SOFTWARE_KEYBOARD_BULLET_THREE='Still not working? Contact your device manufacturer'"'"'s customer support.' +STRING_WOOPOS_SCANNING_SETUP_DEVICE_TERA_1200='Tera 1200' +STRING_WOOPOS_SCANNING_SETUP_DEVICE_STAR_BSH_20B='Star BSH-20B' +STRING_WOOPOS_SCANNING_SETUP_DEVICE_NETUM_1228BC='Netum 1228BC' +STRING_WOOPOS_SCANNING_SETUP_DEVICE_OTHER='Other' +STRING_WOOPOS_SETTINGS_STORE_INFORMATION_TITLE='Store information' +STRING_WOOPOS_SETTINGS_RECEIPT_INFORMATION_TITLE='Receipt information' +STRING_WOOPOS_SETTINGS_STORE_GENERAL_TITLE='General' +STRING_WOOPOS_SETTINGS_STORE_NAME_LABEL='Store name' +STRING_WOOPOS_SETTINGS_STORE_ADDRESS_LABEL='Address' +STRING_WOOPOS_SETTINGS_STORE_PHYSICAL_ADDRESS_LABEL='Physical address' +STRING_WOOPOS_SETTINGS_STORE_PHONE_LABEL='Phone number' +STRING_WOOPOS_SETTINGS_STORE_EMAIL_LABEL='Email' +STRING_WOOPOS_SETTINGS_REFUND_POLICY_LABEL='Refund and return policy' +STRING_WOOPOS_SETTINGS_RECEIPT_EDIT_BUTTON='Edit receipt information' +STRING_WOOPOS_SETTINGS_STORE_NOT_SET='Not set' +STRING_WOOPOS_CARD_READER_CONNECTION_DIALOG_BACKGROUND_CONTENT_DESCRIPTION='Card reader connection dialog' +STRING_WOOPOS_CARD_READER_CLOSE_CONTENT_DESCRIPTION='Close' +STRING_WOOPOS_CARD_READER_SCANNING_TITLE='Scanning for reader' +STRING_WOOPOS_CARD_READER_SCANNING_INSTRUCTION='Make sure your reader is charged and turned on' +STRING_WOOPOS_CARD_READER_SCANNING_FAILED_TITLE='Scanning failed' +STRING_WOOPOS_CARD_READER_FOUND_TITLE_RE='.*\ found' +STRING_WOOPOS_CARD_READER_FOUND_DESCRIPTION='Do you want to connect to this reader?' +STRING_WOOPOS_CARD_READER_CONNECT_BUTTON='Connect' +STRING_WOOPOS_CARD_READER_KEEP_SEARCHING_BUTTON='Keep searching' +STRING_WOOPOS_CARD_READER_MULTIPLE_FOUND_TITLE='Multiple readers found' +STRING_WOOPOS_CARD_READER_MULTIPLE_FOUND_DESCRIPTION='Choose a reader' +STRING_WOOPOS_CARD_READER_PHONE_ICON_CONTENT_DESCRIPTION='Phone reader' +STRING_WOOPOS_CARD_READER_BLUETOOTH_ICON_CONTENT_DESCRIPTION='Bluetooth reader' +STRING_WOOPOS_CARD_READER_UNKNOWN_READER_NAME='Unknown reader' +STRING_WOOPOS_CARD_READER_CONNECTING_TITLE='Connecting' +STRING_WOOPOS_CARD_READER_CONNECTING_MESSAGE='Please wait while we connect to your reader' +STRING_WOOPOS_CARD_READER_CONNECTING_FAILED_TITLE='Connection failed' +STRING_WOOPOS_CARD_READER_CONNECTED_TITLE='Connected' +STRING_WOOPOS_CARD_READER_BATTERY_LOW_TITLE='Battery too low' +STRING_WOOPOS_CARD_READER_BATTERY_LOW_MESSAGE='The reader battery is critically low. Please charge the reader and try again.' +STRING_WOOPOS_CARD_READER_BLUETOOTH_DISABLED_TITLE='Bluetooth is off' +STRING_WOOPOS_CARD_READER_BLUETOOTH_DISABLED_MESSAGE='Enable Bluetooth to connect to a card reader.' +STRING_WOOPOS_CARD_READER_ENABLE_BLUETOOTH_BUTTON='Enable Bluetooth' +STRING_WOOPOS_CARD_READER_LOCATION_DISABLED_TITLE='Location is off' +STRING_WOOPOS_CARD_READER_LOCATION_DISABLED_MESSAGE='Enable Location to connect to a card reader.' +STRING_WOOPOS_CARD_READER_ENABLE_LOCATION_BUTTON='Enable Location' +STRING_WOOPOS_CARD_READER_LOCATION_PERMISSION_TITLE='Location permission required' +STRING_WOOPOS_CARD_READER_LOCATION_PERMISSION_MESSAGE='Location permission is required to connect to a card reader via Bluetooth.' +STRING_WOOPOS_CARD_READER_BLUETOOTH_PERMISSION_TITLE='Bluetooth permission required' +STRING_WOOPOS_CARD_READER_BLUETOOTH_PERMISSION_MESSAGE='Bluetooth permission is required to connect to a card reader.' +STRING_WOOPOS_CARD_READER_GRANT_PERMISSION_BUTTON='Grant permission' +STRING_WOOPOS_CARD_READER_INVALID_ADDRESS_TITLE='Invalid merchant address' +STRING_WOOPOS_CARD_READER_INVALID_ADDRESS_MESSAGE='Your store address is missing or incomplete. Please update your store address in WooCommerce settings.' +STRING_WOOPOS_CARD_READER_INVALID_POSTAL_CODE_TITLE='Invalid postal code' +STRING_WOOPOS_CARD_READER_INVALID_POSTAL_CODE_MESSAGE='Your store postal code is missing or invalid. Please update your store address in WooCommerce settings.' +STRING_WOOPOS_CARD_READER_RETRY_BUTTON='Retry' +STRING_WOOPOS_CARD_READER_CANCEL_BUTTON='Cancel' +STRING_WOOPOS_CARD_READER_UPDATING_TITLE='Updating software' +STRING_WOOPOS_CARD_READER_UPDATING_OPTIONAL_TITLE='Updating your reader'"'"'s software' +STRING_WOOPOS_CARD_READER_UPDATE_PROGRESS_RE='.*%%\ complete' +STRING_WOOPOS_CARD_READER_UPDATE_REQUIRED_MESSAGE='Your card reader software needs to be updated to collect payments. Cancelling will block your reader connection.' +STRING_WOOPOS_CARD_READER_UPDATE_OPTIONAL_MESSAGE='Your card reader'"'"'s software needs to be updated to keep running smoothly' +STRING_WOOPOS_CARD_READER_UPDATE_OPTIONAL_CANCEL_WARNING='Canceling an ongoing software update is not recommended. Cancelling will block your reader connection.' +STRING_WOOPOS_CARD_READER_CANCEL_ANYWAY_BUTTON='Cancel anyway' +STRING_WOOPOS_CARD_READER_UPDATE_COMPLETED_TITLE='Software updated' +STRING_WOOPOS_CARD_READER_UPDATE_FAILED_TITLE='Update failed' +STRING_WOOPOS_CARD_READER_UPDATE_BATTERY_LOW_MESSAGE='The reader battery is too low to install this update. Please charge your reader to at least 50% and try again.' +STRING_WOOPOS_CARD_READER_UPDATE_BATTERY_LOW_MESSAGE_WITH_LEVEL_RE='The\ reader\ battery\ is\ at\ .*%%,\ which\ is\ too\ low\ to\ install\ this\ update\.\ Please\ charge\ your\ reader\ to\ at\ least\ 50%%\ and\ try\ again\.' +STRING_WOOPOS_REMOTE_TTP_HINT_STRIP_TEXT='Use an Android phone as a card reader' +STRING_WOOPOS_REMOTE_TTP_EXPLAINER_TITLE='No card reader? No problem' +STRING_WOOPOS_REMOTE_TTP_EXPLAINER_INTRO='Use the Woo app on your phone to take card payments.' +STRING_WOOPOS_REMOTE_TTP_EXPLAINER_SETUP_HEADING='Set up your phone' +STRING_WOOPOS_REMOTE_TTP_EXPLAINER_SETUP_BODY='Open Woo on your phone, then go to Settings → Payments → Card Reader Mode.' +STRING_WOOPOS_REMOTE_TTP_EXPLAINER_REQUIREMENTS_HEADING='Check compatibility' +STRING_WOOPOS_REMOTE_TTP_EXPLAINER_REQUIREMENTS_BODY='Use the same Wi-Fi and sign in to the same store on both devices. Your phone needs Android 13 or newer, NFC, and Tap-to-Pay support.' +STRING_WOOPOS_REMOTE_TTP_EXPLAINER_GOT_IT='Got it' +STRING_CUSTOMERS_DETAILS_CUSTOMER_SECTION='Customer' +STRING_CUSTOMERS_DETAILS_ORDERS_SECTION='Orders' +STRING_CUSTOMERS_DETAILS_REGISTRATION_SECTION='Registration' +STRING_CUSTOMERS_DETAILS_BILLING_ADDRESS_SECTION='Billing Address' +STRING_CUSTOMERS_DETAILS_SHIPPING_ADDRESS_SECTION='Shipping Address' +STRING_CUSTOMERS_DETAILS_SHIPPING_LOCATION_SECTION='Location' +STRING_CUSTOMERS_DETAILS_LAST_ACTIVE_VALUE_TITLE='Last active' +STRING_CUSTOMERS_DETAILS_ORDERS_VALUE_TITLE='Orders' +STRING_CUSTOMERS_DETAILS_TOTAL_SPEND_VALUE_TITLE='Total spend' +STRING_CUSTOMERS_DETAILS_AVERAGE_ORDER_VALUE_TITLE='Average order value' +STRING_CUSTOMERS_DETAILS_USERNAME_VALUE_TITLE='Username' +STRING_CUSTOMERS_DETAILS_DATE_REGISTERED_VALUE_TITLE='Date registered' +STRING_CUSTOMERS_DETAILS_COUNTRY_VALUE_TITLE='Country' +STRING_CUSTOMERS_DETAILS_REGION_VALUE_TITLE='Region' +STRING_CUSTOMERS_DETAILS_CITY_VALUE_TITLE='City' +STRING_CUSTOMERS_DETAILS_POSTAL_CODE_VALUE_TITLE='Postal code' +STRING_CUSTOMERS_DETAILS_NO_PHONE_HINT='No phone number' +STRING_CUSTOMERS_DETAILS_NONE_HINT='None' +STRING_GOOGLE_ADS_CAMPAIGN_CREATED_SUCCESS_TITLE='Ready to Go!' +STRING_GOOGLE_ADS_CAMPAIGN_CREATED_SUCCESS_DESCRIPTION='Your new campaign has been created. Exciting times ahead for your sales!' +STRING_CUSTOM_FIELDS_LIST_TITLE='Custom Fields' +STRING_CUSTOM_FIELDS_LIST_LOADING_ERROR='Error while loading custom fields' +STRING_CUSTOM_FIELDS_LIST_PROGRESS_DIALOG_TITLE='Saving changes' +STRING_CUSTOM_FIELDS_LIST_SAVING_SUCCEEDED='Changes saved' +STRING_CUSTOM_FIELDS_LIST_SAVING_FAILED='Saving changes failed, please try again' +STRING_CUSTOM_FIELDS_LIST_FIELD_DELETED='Custom Field deleted' +STRING_CUSTOM_FIELDS_LIST_TOP_BANNER_TITLE='View and edit Custom Fields' +STRING_CUSTOM_FIELDS_LIST_TOP_BANNER_MESSAGE='When saving changes to custom fields, they will take effect immediately.' +STRING_CUSTOM_FIELDS_ADD_BUTTON='Add custom fields' +STRING_CUSTOM_FIELDS_EMPTY_VIEW_MESSAGE='Custom fields are optional metadata to display extra information or customize your store’s shopping experience.' +STRING_CUSTOM_FIELDS_EMPTY_VIEW_TITLE='No custom fields found' +STRING_CUSTOM_FIELDS_EDITOR_KEY_LABEL='Key' +STRING_CUSTOM_FIELDS_EDITOR_VALUE_LABEL='Value' +STRING_CUSTOM_FIELDS_EDITOR_KEY_ERROR_DUPLICATE='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.' +STRING_CUSTOM_FIELDS_EDITOR_KEY_ERROR_UNDERSCORE='Invalid key: please remove the "_" character from the beginning.' +STRING_CUSTOM_FIELDS_EDITOR_COPY_KEY='Copy Key' +STRING_CUSTOM_FIELDS_EDITOR_COPY_VALUE='Copy Value' +STRING_CUSTOM_FIELDS_EDITOR_TOGGLE_ACCESSIBILITY_DESCRIPTION='Toggle between text and HTML editors' +STRING_CUSTOM_FIELDS_EDITOR_TEXT_TOGGLE='Text' +STRING_CUSTOM_FIELDS_EDITOR_HTML_TOGGLE='HTML' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_TAB_CUSTOM='Custom' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_TAB_CARRIER='Carrier' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_TAB_SAVED='Saved' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_PACKAGE_TYPE='Package type' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_LENGTH='Length' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_WIDTH='Width' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_HEIGHT='Height' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_WEIGHT='Weight' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_PACKAGE_NAME='Package Name' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_INVALID_DIMENSION='Package dimensions should all be larger than 0.' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_SAVE_PACKAGE_OPTION='Save this as a new package template' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_ADD_PACKAGE='Add Package' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_ADD_PACKAGE_DETAILS='Add Package Details' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_EMPTY_BUTTON='Create a custom package' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_EMPTY_CARRIER_MESSAGE='No carrier information found' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_EMPTY_SAVED_MESSAGE='No saved packages yet' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_CARRIER_LOADING_ERROR='We are unable to load carrier packages.' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_SAVED_LOADING_ERROR='We are unable to load saved packages.' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_SHIPPING_RATES_LOADING_ERROR='We are unable to load shipping rates.' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_SHIPPING_RATES_DESTINATION_NAME_ERROR='We couldn'"'"'t load shipping rates. Please add a first and last name to the destination address and try again.' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_SHIPPING_RATES_EMPTY='We couldn'"'"'t find a shipping service for the combination of the selected package and the total shipment weight. Please adjust your input and try again.' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_SHIPPING_RATES_EMPTY_WITH_HAZMAT='We couldn'"'"'t find a shipping service for the combination of the selected HAZMAT category, the selected package and the total shipment weight. Please adjust your input and try again.' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_STARRED='Star this item' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_UNSTARRED='Unstar this item' +STRING_WOO_SHIPPING_LABELS_SHIPPING_RATES_MISSING_DESTINATION='Add a destination address to get shipping rates' +STRING_WOO_SHIPPING_LABELS_SHIPPING_RATES_MISSING_DESTINATION_DESC='We need to know where this package is going before we can show the available shipping rates.' +STRING_WOO_SHIPPING_LABELS_SHIPPING_RATES_MISSING_WEIGHT='Add shipment weight to get shipping rates' +STRING_WOO_SHIPPING_LABELS_SHIPPING_RATES_MISSING_WEIGHT_DESC='We need to know the shipment weight before we can show the available shipping rates.' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_BOX_TYPE='Box' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_ENVELOPE_TYPE='Envelope' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_ERROR_TITLE='We couldn'"'"'t save the package as template' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_ERROR_MESSAGE='Do you want to proceed without saving it?' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_ERROR_PROCEED='Proceed' +STRING_WOO_SHIPPING_LABELS_PACKAGE_CREATION_ERROR_CANCEL='Cancel' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_ADD_MISSING_INFORMATION='Add Missing Information' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_CONTENT_TYPE_LABEL='Content Type' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_RESTRICTION_TYPE_LABEL='Restriction Type' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_ITN_LABEL='International Transaction Number' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_RETURN_TO_SENDER_LABEL='Return to sender if package is not able to be delivered' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_CONTENT_DETAILS_LABEL='Content Details' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_CONTENT_DETAILS_DESCRIPTION='Please describe what kind of goods this package contains' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_CONTENT_MERCHANDISE='Merchandise' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_CONTENT_GIFT='Gift' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_CONTENT_RETURNED_GOODS='Returned Goods' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_CONTENT_SAMPLE='Sample' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_CONTENT_DOCUMENTS='Documents' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_CONTENT_OTHER='Other' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_RESTRICTION_DETAILS_LABEL='Restriction Details' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_RESTRICTION_DETAILS_DESCRIPTION='Please describe what kind of restrictions this package must have' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_RESTRICTION_NONE='None' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_RESTRICTION_QUARANTINE='Quarantine' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_RESTRICTION_SANITARY='Sanitary / Phytosanitary inspection' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_RESTRICTION_OTHER='Other' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_OTHER_ERROR_MESSAGE='Type must not be empty' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_ITN_ERROR_MESSAGE='Invalid ITN format' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_ITN_REQUIRED_ERROR='ITN is required' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_ITN_REQUIRED_TOTAL_VALUE='For shipments over $2,500, you need to obtain a 14-digit AES ITN for U.S. export reporting verification.' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_ITN_REQUIRED_HS_TARIFF_VALUE_RE='International\ Transaction\ Number\ is\ required\ for\ shipping\ items\ valued\ over\ \$2,500\ per\ tariff\ number\.\\nProducts\ with\ tariff\ number\ .*\ add\ up\ to\ more\ than\ \$2,500\.' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_ITN_REQUIRED_DESTINATION_COUNTRY='International Transaction Number is required for shipments to the destination country.' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_TITLE='Product Details' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_DESCRIPTION='Description' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_DESCRIPTION_MISSING='Missing Description' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_DESCRIPTION_TOO_LONG_RE='Description\ must\ be\ .*\ characters\ or\ fewer\.' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_TARIFF='HS tariff number' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_TARIFF_INVALID='The tariff number must be between 6 and 12 digits long' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_VALUE_PER_UNIT='Value per unit' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_WEIGHT_PER_UNIT='Weight per unit' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_WEIGHT_INVALID='Weight must be greater than zero' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_VALUE_REQUIRED='Value required' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_ORIGIN_COUNTRY='Origin country' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_ORIGIN_COUNTRY_MISSING='Missing country' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_ORIGIN_COUNTRY_SELECTION='Select a country' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_ITN_INFO_BUTTON='More info about ITN' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_HS_TARIFF_INFO_BUTTON='More info about HS tariff' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_DESCRIPTION_INFO_BUTTON='More info about product description' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_DESCRIPTION_INFO='When shipping to countries that follow European Union (EU) customs rules, you must provide a clear, specific description on every item. For example, if you are sending clothing, you must indicate what type of clothing (e.g. men'"'"'s shirts, girl'"'"'s vest, boy'"'"'s jacket) for the description to be acceptable. Otherwise, shipments may be delayed or interrupted at customs.' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_ORIGIN_COUNTRY_INFO_BUTTON='More info about product origin country' +STRING_WOO_SHIPPING_LABELS_CUSTOMS_ORIGIN_COUNTRY_INFO='Country where the product was manufactured or assembled.' +STRING_WOO_SHIPPING_LABELS_HAZMAT_INFO_TITLE='Are you shipping dangerous goods or hazardous materials?' +STRING_WOO_SHIPPING_LABELS_HAZMAT_INFO_CONTAINS_HAZMAT='Contains hazardous materials' +STRING_WOO_SHIPPING_LABELS_HAZMAT_INFO_FULL_DESCRIPTION='Potentially hazardous material includes items such as batteries, dry ice, flammable liquids, aerosols, ammunition, fireworks, nail polish, perfume, paint, solvents, and more. Hazardous items must ship in separate packages.' +STRING_WOO_SHIPPING_LABELS_HAZMAT_INFO_TOOLTIP_1='Learn how to securely package, label, and ship HAZMAT through USPS® at www.usps.com/hazmat. Determine your product'"'"'s mailability using the USPS HAZMAT Search Tool.' +STRING_WOO_SHIPPING_LABELS_HAZMAT_INFO_TOOLTIP_2='WooCommerce Shipping does not currently support HAZMAT shipments through DHL Express.' +STRING_WOO_SHIPPING_LABELS_HAZMAT_INFO_SELECT_CATEGORY='Select Category' +STRING_WOO_SHIPPING_LABELS_HAZMAT_SEARCH_HINT='Search a Category' +STRING_WOO_SHIPPING_LABELS_HAZMAT_SELECTION_SET='Hazardous materials category set' +STRING_WOO_SHIPPING_LABELS_HAZMAT_SELECTION_REMOVED='Removed hazardous materials category' +STRING_EMAIL_NOT_REGISTERED_WPCOM='Hmm, we can'"'"'t find a WordPress.com account connected to this email address.' +STRING_WOO_SHIPPING_EDIT_ORIGIN_ADDRESS_TITLE='Edit Origin' +STRING_WOO_SHIPPING_EDIT_DESTINATION_ADDRESS_TITLE='Edit Destination' +STRING_WOO_SHIPPING_LABEL_NAME='Name' +STRING_WOO_SHIPPING_LABEL_COMPANY='Company' +STRING_WOO_SHIPPING_LABEL_COUNTRY='Country' +STRING_WOO_SHIPPING_LABEL_ADDRESS='Address' +STRING_WOO_SHIPPING_LABEL_CITY='City' +STRING_WOO_SHIPPING_LABEL_STATE='State' +STRING_WOO_SHIPPING_LABEL_POST_CODE='Postal Code' +STRING_WOO_SHIPPING_LABEL_EMAIL='Email' +STRING_WOO_SHIPPING_LABEL_PHONE='Phone' +STRING_WOO_SHIPPING_FIELD_REQUIRED_ERROR='This field is required.' +STRING_WOO_SHIPPING_FETCHING_COUNTRIES_AND_STATES_FAILED='We were unable to retrieve country and state information. Please try again.' +STRING_WOO_SHIPPING_FETCHING_COUNTRIES_AND_STATES='Fetching countries and states.' +STRING_WOO_SHIPPING_ADDRESS_VERIFIED='Address verified' +STRING_WOO_SHIPPING_ADDRESS_UNVERIFIED='Unverified address' +STRING_WOO_SHIPPING_ADDRESS_MISSING_INFO='Missing information' +STRING_WOO_SHIPPING_ADDRESS_MISSING='Missing address' +STRING_WOO_SHIPPING_ADDRESS_UNSAVED_CHANGES='Unsaved changes' +STRING_WOO_SHIPPING_VERIFYING_ADDRESS_FAILED='The address verification failed. Please try again.' +STRING_WOO_SHIPPING_UPDATING_ADDRESS_FAILED='We couldn'"'"'t update your address. Please try again.' +STRING_WOO_SHIPPING_ADDRESS_NOTIFICATION_DESTINATION_UNVERIFIED='Destination address unverified' +STRING_WOO_SHIPPING_ADDRESS_NOTIFICATION_ORIGIN_UNVERIFIED='Origin address unverified' +STRING_WOO_SHIPPING_ADDRESS_NOTIFICATION_ORIGIN_MISSING_OR_INVALID='Origin address missing or invalid' +STRING_WOO_SHIPPING_ADDRESS_NOTIFICATION_DESTINATION_MISSING_OR_INVALID='Destination address missing or invalid' +STRING_WOO_SHIPPING_ADDRESS_NOTIFICATION_DESTINATION_VERIFIED='Verified destination address' +STRING_WOO_SHIPPING_ADDRESS_NOTIFICATION_ORIGIN_VERIFIED='Verified origin address' +STRING_WOO_SHIPPING_ADDRESS_VALIDATE_AND_SAVE='Validate & Save' +STRING_WOO_SHIPPING_ADDRESS_MISSING_INFO_HINT='Add missing information' +STRING_WOO_SHIPPING_ADDRESS_USE_AS_ENTERED='Use address as entered' +STRING_WOO_SHIPPING_LABELS_LOADING_ORDER_ERROR='Unable to load the order' +STRING_WOO_SHIPPING_LABELS_PACKAGE_REMOVING_ERROR='Unable to remove the package' +STRING_WOO_SHIPPING_LABELS_PACKAGE_SAVING_ERROR='Unable to save the package' +STRING_WOO_SHIPPING_LABELS_PURCHASE_ERROR='We are unable to purchase the shipping label. Please try again.' +STRING_WOO_SHIPPING_LABELS_PURCHASE_ORIGIN_ADDRESS_ERROR='Unable to purchase shipping label. Please complete the origin address.' +STRING_WOO_SHIPPING_LABELS_PURCHASE_PHONE_ERROR='Unable to purchase shipping label. Please add a valid phone number to the shipping address.' +STRING_WOO_SHIPPING_LABELS_ORDER_COMPLETION_ERROR='Couldn'"'"'t mark the order as complete' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_ERROR='We were unable to split the shipments. Please try again.' +STRING_WOO_SHIPPING_LABELS_LOADING_ERROR='Unable to load data' +STRING_WOO_SHIPPING_ADDRESS_SAVE_CHANGES='Save changes' +STRING_WOO_SHIPPING_ADDRESS_VALIDATE_TITLE='Validating' +STRING_WOO_SHIPPING_ADDRESS_VALIDATE_MESSAGE='Validating entered address' +STRING_WOO_SHIPPING_ADDRESS_UPDATE_TITLE='Updating' +STRING_WOO_SHIPPING_ADDRESS_UPDATE_MESSAGE='Updating entered address' +STRING_WOO_SHIPPING_CONFIRM_ADDRESS_TITLE='Confirm address' +STRING_WOO_SHIPPING_CONFIRM_ADDRESS_DESCRIPTION='We have slightly modified the entered address.\nIf correct, please use the suggested address to ensure accurate delivery.' +STRING_WOO_SHIPPING_CONFIRM_ADDRESS_ENTERED='What you entered' +STRING_WOO_SHIPPING_CONFIRM_ADDRESS_SUGGESTED='Suggested' +STRING_WOO_SHIPPING_CONFIRM_SUBMIT_ADDRESS_SUGGESTED='Confirm Suggested Address' +STRING_WOO_SHIPPING_CONFIRM_SUBMIT_ADDRESS_ENTERED='Confirm Entered Address' +STRING_ERROR_USER_USERNAME_INSTEAD_OF_EMAIL='Please log in using your WordPress.com username instead of your email address.' +STRING_DEFAULT_WEB_CLIENT_ID='placeholder' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT='Split shipment' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_PURCHASED_MESSAGE_TITLE='You purchased a label for this shipment.' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_PURCHASED_MESSAGE_DESC='You can’t move products in to or out of it.' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_INSTRUCTIONS='To split, select the items, and tap move to new shipment when the toolbar appears.' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_MOVED_NOTICE_ONE_RE='Moved\ .*\ item\ to\ Shipment\ .*' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_MOVED_NOTICE_PLURAL_RE='Moved\ .*\ items\ to\ Shipment\ .*' +STRING_WOO_SHIPPING_SPLIT_MOVE_TO_NEW='Move to new shipment' +STRING_WOO_SHIPPING_SPLIT_MOVE_TO='Move to' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_SHIPMENT_NAME_RE='Shipment\ .*' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_SHIPMENT_NEW='New shipment' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_SHIPMENT_REMOVE_RE='Remove\ .*' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_BOTTOM_SHEET_TITLE='Remove shipment' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_BOTTOM_SHEET_DESC_RE='All\ items\ will\ be\ merged\ with\ .*\.' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_BOTTOM_SHEET_DESC_MULTIPLE_SHIPMENT_ONE_RE='Choose\ where\ to\ move\ the\ .*\ item\ in\ this\ shipment\ to\.' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_BOTTOM_SHEET_DESC_MULTIPLE_SHIPMENT_PLURAL_RE='Choose\ where\ to\ move\ the\ .*\ items\ in\ this\ shipment\ to\.' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_BOTTOM_SHEET_REMOVE_BUTTON='Remove Shipment' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_MERGE_UNFULFILLED_MENU='Merge all unfulfilled' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_MERGE_UNFULFILLED_TITLE='Merge all unfulfilled shipments' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_MERGE_UNFULFILLED_DESC='This will remove all unfulfilled split shipments and move all items into one shipment.' +STRING_WOO_SHIPPING_SPLIT_SHIPMENT_MERGE_UNFULFILLED_BUTTON='Merge all shipments' +STRING_PURCHASED_SHIPMENT_CONTENT_DESCRIPTION='Purchased' +STRING_WOO_SHIPPING_REFUND_TITLE='Request a shipping label refund' +STRING_WOO_SHIPPING_REFUND_DURATION_DESCRIPTION_RE='Request\ a\ refund\ for\ your\ unused\ shipping\ label\.\ The\ refund\ process\ for\ the\ shipping\ label\ will\ begin\ immediately\ and\ is\ typically\ completed\ within\ .*\ business\ days\.' +STRING_WOO_SHIPPING_REFUND_PURCHASE_DATE_RE='Purchase\ date:\ .*' +STRING_WOO_SHIPPING_REFUND_AMOUNT_ELIGIBLE_FOR_REFUND_RE='Amount\ eligible\ for\ refund:\ .*' +STRING_WOO_SHIPPING_REFUND_NOTE='Please note that this refund request applies only to the unused shipping label and will not affect the order itself.' +STRING_WOO_SHIPPING_PAYMENT_SCREEN_TITLE='Payment method' +STRING_WOO_SHIPPING_PAYMENT_SCREEN_DESCRIPTION='Choose a payment method.' +STRING_WOO_SHIPPING_PAYMENT_EDIT_DISABLED_WARNING_RE='Only\ the\ site\ owner\ can\ manage\ the\ shipping\ label\ payment\ methods\.\ Please\ contact\ Store\ Owner\ .*\ \(.*\)\ to\ manage\ payment\ methods\.' +STRING_WOO_SHIPPING_PAYMENT_NO_PAYMENT_METHODS_TITLE='Add a payment method' +STRING_WOO_SHIPPING_PAYMENT_NO_PAYMENT_METHODS_DESCRIPTION='Add a payment method to purchase a shipping label' +STRING_WOO_SHIPPING_PAYMENT_ADD_NEW_BUTTON='New credit or debit card' +STRING_WOO_SHIPPING_PAYMENT_METHODS_INFO_FOOTER_RE='Credit\ cards\ are\ retrieved\ from\ the\ following\ WordPress\.com\ account:\ .*\ <.*>' +STRING_WOO_SHIPPING_PAYMENT_EMAIL_RECEIPT_TOGGLE='Email the receipt' +STRING_WOO_SHIPPING_PAYMENT_USE_CARD_BUTTON='Use this card' +STRING_WOO_SHIPPING_PAYMENT_METHOD_ADDED='Payment method added' +STRING_WOO_SHIPPING_PAYMENT_FETCHING_CARDS_FAILED='Unable to refresh your payment methods' +STRING_WPP_SHIPPING_UPS_TOS_TITLE='UPS® Terms and Conditions' +STRING_WPP_SHIPPING_UPS_TOS_SHIPPING_FROM='Shipping from' +STRING_WPP_SHIPPING_UPS_TOS_DESCRIPTION='To start shipping from this address with UPS®, we need you to agree to the following terms and conditions:' +STRING_WPP_SHIPPING_UPS_TOS_CONDITION_TERMS='I agree to the UPS® Terms of Service.' +STRING_WPP_SHIPPING_UPS_TOS_CONDITION_PROHIBITED_ITEMS='I will not ship any Prohibited Items that UPS® disallows, nor any regulated items without the necessary permissions.' +STRING_WPP_SHIPPING_UPS_TOS_CONDITION_TECHNOLOGY_AGREEMENT='I also agree to the UPS® Technology Agreement.' +STRING_WPP_SHIPPING_UPS_TOS_ACCEPT='Confirm and continue' +STRING_WPP_SHIPPING_FEDEX_TOS_TITLE='FedEx Terms of Service' +STRING_WPP_SHIPPING_FEDEX_TOS_DESCRIPTION='To purchase FedEx shipping labels, you need to agree to the following terms:' +STRING_WPP_SHIPPING_FEDEX_TOS_CONDITION_TERMS='I agree to the FedEx Terms of Service.' +STRING_WOO_SHIPPING_RATE_EXTRA_COST_FORMAT_RE='\+.*' +STRING_WOO_SHIPPING_RATE_SURCHARGE_DESCRIPTION_TEMPLATE_RE='.*\ \(.*\)' +STRING_WOO_SHIPPING_RATE_OPTION_SIGNATURE_REQUIRED='Signature required' +STRING_WOO_SHIPPING_RATE_OPTION_ADULT_SIGNATURE_REQUIRED='Adult signature required' +STRING_WOO_SHIPPING_RATE_OPTION_CARBON_NEUTRAL='Carbon neutral' +STRING_WOO_SHIPPING_RATE_OPTION_SATURDAY_DELIVERY='Saturday delivery' +STRING_WOO_SHIPPING_RATE_OPTION_ADDITIONAL_HANDLING='Additional handling' +STRING_MEDIA_LIBRARY_TITLE='WordPress Media Library' +STRING_MEDIA_LOADING_FAILED='Media loading failed' +STRING_NO_NETWORK_MESSAGE='There is no network available' +STRING_OR_USE_PASSWORD='Use password to sign in' +STRING_ABOUT_AUTOMATTIC_MAIN_PAGE_TITLE_RE='About\ .*' +STRING_ABOUT_AUTOMATTIC_LEGAL_PAGE_TITLE='Legal and More' +STRING_ABOUT_AUTOMATTIC_ACKNOWLEDGEMENTS_PAGE_TITLE='Acknowledgements' +STRING_ABOUT_AUTOMATTIC_SHARE_WITH_FRIENDS_ITEM_TITLE='Share with Friends' +STRING_ABOUT_AUTOMATTIC_RATE_US_ITEM_TITLE='Rate Us' +STRING_ABOUT_AUTOMATTIC_INSTAGRAM_ITEM_TITLE='Instagram' +STRING_ABOUT_AUTOMATTIC_X_ITEM_TITLE='X' +STRING_ABOUT_AUTOMATTIC_LEGAL_AND_MORE_ITEM_TITLE='Legal and More' +STRING_ABOUT_AUTOMATTIC_FAMILY_ITEM_TITLE='Automattic Family' +STRING_ABOUT_AUTOMATTIC_TERMS_OF_SERVICE_ITEM_TITLE='Terms of Service' +STRING_ABOUT_AUTOMATTIC_PRIVACY_POLICY_ITEM_TITLE='Privacy Policy' +STRING_ABOUT_AUTOMATTIC_CALIFORNIA_PRIVACY_NOTICE_ITEM_TITLE='California Privacy Notice' +STRING_ABOUT_AUTOMATTIC_SOURCE_CODE_ITEM_TITLE='Source Code' +STRING_ABOUT_AUTOMATTIC_ACKNOWLEDGEMENTS_ITEM_TITLE='Acknowledgements' +STRING_ABOUT_AUTOMATTIC_DAYONE='Day One' +STRING_ABOUT_AUTOMATTIC_JETPACK='Jetpack' +STRING_ABOUT_AUTOMATTIC_POCKETCASTS='Pocket Casts' +STRING_ABOUT_AUTOMATTIC_SIMPLENOTE='Simplenote' +STRING_ABOUT_AUTOMATTIC_TUMBLR='Tumblr' +STRING_ABOUT_AUTOMATTIC_WOOCOMMERCE='WooCommerce' +STRING_ABOUT_AUTOMATTIC_WORDPRESS='WordPress' +STRING_ABOUT_AUTOMATTIC_VERSION_LABEL_RE='Version\ .*' +STRING_ABOUT_AUTOMATTIC_LOGO_DESCRIPTION='Automattic logo' +STRING_ABOUT_AUTOMATTIC_BACK_ICON_DESCRIPTION='Back icon' +STRING_ABOUT_AUTOMATTIC_APP_ICON_DESCRIPTION='App icon' +STRING_AGE_RESTRICTION_DIALOG_TITLE='Account Access Restricted' +STRING_AGE_RESTRICTION_SUPERVISED_USER_ACCOUNT_DIALOG_MESSAGE='Your Google Account settings indicate that you need a parent or guardian'"'"'s permission to continue. They can grant access using their Google account to get you back online.' +STRING_AGE_RESTRICTION_USER_BELOW_TOS_MINIMUM_AGE_DIALOG_MESSAGE='Your Google Account indicates that you are under 13 years old, which is below the minimum age allowed to use the WooCommerce platform under its terms of service.' +STRING_WOO_POS_PROMO_TITLE='Point of Sale from WooCommerce' +STRING_WOO_POS_PROMO_PAGE1_DESCRIPTION='Take payments in person and connect everything back to your store — all through the WooCommerce mobile app.' +STRING_WOO_POS_PROMO_PAGE2_DESCRIPTION='Real-time syncing for inventory, customer, and order data between online and in-person channels.' +STRING_WOO_POS_PROMO_PAGE3_DESCRIPTION='Quick and simple to learn.' +STRING_WOO_POS_PROMO_PAGE4_DESCRIPTION='Built right into the WooCommerce mobile app — no extra plugins needed.' +STRING_WOO_POS_PROMO_PAGE5_DESCRIPTION='Use with WooPayments or Stripe payment gateways.' +STRING_WOO_POS_PROMO_NEXT_BUTTON='Next' +STRING_WOO_POS_PROMO_EXPLORE_BUTTON='Explore WooCommerce POS' +STRING_WOOPOS_ELIGIBILITY_SCREEN_US_COUNTRY_NAME='the United States' +STRING_WOOPOS_ELIGIBILITY_SCREEN_UK_COUNTRY_NAME='the United Kingdom' +STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_GENERIC='Update an order' +STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_TITLE_RE='Update\ order\ \#.*' +STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_SUMMARY_RE='Update\ order\ \#.*:\ emails\ the\ customer' +STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_GENERIC='Update many orders' +STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_TITLE_SINGLE_RE='Update\ .*\ order' +STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_TITLE_MULTIPLE_RE='Update\ .*\ orders' +STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_SUMMARY_SINGLE_RE='Update\ .*\ order:\ emails\ the\ customer' +STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_SUMMARY_MULTIPLE_RE='Update\ .*\ orders:\ emails\ customers' +STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_UPDATE_GENERIC='Update a product' +STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_UPDATE_TITLE_RE='Update\ product\ \#.*' +STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_UPDATE_TITLE_WITH_NAME_RE='Update\ product\ .*\ \(\#.*\)' +STRING_AI_ASSISTANT_CONFIRMATION_PRODUCTS_BULK_UPDATE_GENERIC='Update many products' +STRING_AI_ASSISTANT_CONFIRMATION_PRODUCTS_BULK_UPDATE_TITLE_SINGLE_RE='Update\ .*\ product' +STRING_AI_ASSISTANT_CONFIRMATION_PRODUCTS_BULK_UPDATE_TITLE_MULTIPLE_RE='Update\ .*\ products' +STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_VARIATION_UPDATE_GENERIC='Update a product variation' +STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_VARIATION_UPDATE_TITLE_RE='Update\ variation\ \#.*\ for\ product\ \#.*' +STRING_AI_ASSISTANT_CONFIRMATION_GENERIC_TOOL_CALL_RE='Review\ .*' +STRING_AI_ASSISTANT_CONFIRMATION_FIELD_STATUS='Status' +STRING_AI_ASSISTANT_CONFIRMATION_FIELD_CUSTOMER_NOTE='Customer note' +STRING_AI_ASSISTANT_CONFIRMATION_FIELD_BILLING_EMAIL='Billing email' +STRING_AI_ASSISTANT_CONFIRMATION_FIELD_NAME='Name' +STRING_AI_ASSISTANT_CONFIRMATION_FIELD_REGULAR_PRICE='Price' +STRING_AI_ASSISTANT_CONFIRMATION_FIELD_SALE_PRICE='Sale' +STRING_AI_ASSISTANT_CONFIRMATION_FIELD_STOCK_QUANTITY='Stock' +STRING_AI_ASSISTANT_CONFIRMATION_FIELD_STOCK_STATUS='Stock status' +STRING_AI_ASSISTANT_CONFIRMATION_FIELD_SKU='SKU' +STRING_AI_ASSISTANT_CONFIRMATION_FIELD_VALUE_UPDATED='Updated' +STRING_AI_ASSISTANT_CONFIRMATION_FIELD_VALUE_OFF='Off' +STRING_AI_ASSISTANT_CONFIRMATION_EYEBROW_PENDING='Pending change' +STRING_AI_ASSISTANT_CONFIRMATION_EYEBROW_CONFIRMED='Confirmed' +STRING_AI_ASSISTANT_CONFIRMATION_EYEBROW_CANCELLED='Cancelled' +STRING_AI_ASSISTANT_CONFIRMATION_BULK_ENTRIES_OVERFLOW_RE='\+.*\ more' +STRING_AI_ASSISTANT_CHAT_TITLE='Assistant' +STRING_AI_ASSISTANT_CHAT_BACK_CONTENT_DESCRIPTION='Back' +STRING_AI_ASSISTANT_CHAT_RESTART_CONTENT_DESCRIPTION='Start new conversation' +STRING_AI_ASSISTANT_CHAT_JUMP_TO_LATEST_CONTENT_DESCRIPTION='Jump to latest' +STRING_AI_ASSISTANT_CHAT_MESSAGE_USER_CONTENT_DESCRIPTION_RE='You:\ .*' +STRING_AI_ASSISTANT_CHAT_MESSAGE_ASSISTANT_CONTENT_DESCRIPTION_RE='Assistant:\ .*' +STRING_AI_ASSISTANT_CHAT_TYPING_CONTENT_DESCRIPTION='Assistant is thinking' +STRING_AI_ASSISTANT_CHAT_TOOL_ACTIVITY_ORDERS_READ='Checking orders' +STRING_AI_ASSISTANT_CHAT_TOOL_ACTIVITY_ORDERS_WRITE='Updating orders' +STRING_AI_ASSISTANT_CHAT_TOOL_ACTIVITY_PRODUCTS_READ='Checking products' +STRING_AI_ASSISTANT_CHAT_TOOL_ACTIVITY_PRODUCTS_WRITE='Updating products' +STRING_AI_ASSISTANT_CHAT_TOOL_ACTIVITY_ANALYTICS='Checking analytics' +STRING_AI_ASSISTANT_CHAT_TOOL_ACTIVITY_CUSTOMERS='Checking customers' +STRING_AI_ASSISTANT_CHAT_TOOL_ACTIVITY_GENERIC='Checking your store' +STRING_AI_ASSISTANT_CHAT_TOOL_ACTIVITY_CONTENT_DESCRIPTION_RE='Assistant\ is\ working:\ .*' +STRING_AI_ASSISTANT_CHAT_CARD_GROUP_ORDERS='Orders' +STRING_AI_ASSISTANT_CHAT_CARD_GROUP_PRODUCTS='Products' +STRING_AI_ASSISTANT_CHAT_CARD_GROUP_VARIATIONS='Variations' +STRING_AI_ASSISTANT_CHAT_CARD_GROUP_STATS='Analytics' +STRING_AI_ASSISTANT_CHAT_CARD_GROUP_CUSTOMERS='Customers' +STRING_AI_ASSISTANT_CHAT_CARD_GROUP_GENERIC='Cards' +STRING_AI_ASSISTANT_CHAT_EMPTY_STATE_TITLE='What can I help with?' +STRING_AI_ASSISTANT_CHAT_EMPTY_STATE_SUGGESTION_REVENUE='How'"'"'s revenue this week?' +STRING_AI_ASSISTANT_CHAT_EMPTY_STATE_SUGGESTION_STOCK='What'"'"'s running low?' +STRING_AI_ASSISTANT_CHAT_EMPTY_STATE_SUGGESTION_ORDERS='Any orders need my attention?' +STRING_AI_ASSISTANT_CHAT_EMPTY_STATE_SUGGESTION_CUSTOMERS='Who are my newest customers?' +STRING_AI_ASSISTANT_CHAT_EMPTY_STATE_PROMPT_REVENUE='How'"'"'s my revenue this week? Show me total sales for this week and how it compares to last week.' +STRING_AI_ASSISTANT_CHAT_EMPTY_STATE_PROMPT_STOCK='What'"'"'s running low? List the products that are out of stock or low on inventory so I know what to restock.' +STRING_AI_ASSISTANT_CHAT_EMPTY_STATE_PROMPT_ORDERS='Any orders that need my attention? Show me recent orders that are pending, on hold, or processing.' +STRING_AI_ASSISTANT_CHAT_EMPTY_STATE_PROMPT_CUSTOMERS='Show my newest customers. List up to 10 customers sorted by registration date, newest first.' +STRING_AI_ASSISTANT_CHAT_EMPTY_STATE_SUGGESTION_CONTENT_DESCRIPTION_RE='Send\ suggestion:\ .*' +STRING_AI_ASSISTANT_EARLY_ACCESS_NOTICE_BADGE='EARLY ACCESS' +STRING_AI_ASSISTANT_EARLY_ACCESS_NOTICE_BODY='Hi! We'"'"'re just getting started, with more on the way. Let us know what'"'"'s missing and what you'"'"'d love help with!' +STRING_AI_ASSISTANT_EARLY_ACCESS_NOTICE_FEEDBACK='Give Feedback' +STRING_AI_ASSISTANT_EARLY_ACCESS_NOTICE_DISMISS_CONTENT_DESCRIPTION='Dismiss early access notice' +STRING_AI_ASSISTANT_STATS_CARD_TOTAL_SALES_LABEL='Total Sales' +STRING_AI_ASSISTANT_STATS_CARD_NET_SALES_LABEL='Net Sales' +STRING_AI_ASSISTANT_STATS_CARD_TOTAL_ORDERS_LABEL='Total Orders' +STRING_AI_ASSISTANT_STATS_CARD_AVERAGE_ORDER_VALUE_LABEL='Average Order Value' +STRING_AI_ASSISTANT_STATS_CARD_METRIC_UNAVAILABLE='Unavailable' +STRING_AI_ASSISTANT_STATS_CARD_OPEN_CONTENT_DESCRIPTION_RE='Open\ analytics\ for\ .*' +STRING_AI_ASSISTANT_CHAT_PLACEHOLDER='Ask about your store' +STRING_AI_ASSISTANT_CHAT_SEND_CONTENT_DESCRIPTION='Send message' +STRING_AI_ASSISTANT_CHAT_STOP_CONTENT_DESCRIPTION='Stop response' +STRING_AI_ASSISTANT_CHAT_PENDING_CONFIRMATION_HINT='Resolve the pending change above.' +STRING_AI_ASSISTANT_CHAT_RETRY='Retry' +STRING_AI_ASSISTANT_CHAT_CONFIRM_TOOL_RE='Confirm\ .*\?' +STRING_AI_ASSISTANT_CHAT_CONFIRM='Confirm' +STRING_AI_ASSISTANT_CHAT_CANCEL='Cancel' +STRING_AI_ASSISTANT_CHAT_ERROR_NETWORK='Couldn'"'"'t connect. Check your connection and try again.' +STRING_AI_ASSISTANT_CHAT_ERROR_AUTH='The assistant couldn'"'"'t access your store. Please sign in again, then reopen the assistant.' +STRING_AI_ASSISTANT_CHAT_ERROR_RATE_LIMIT='The assistant is busy right now. Try again in a moment.' +STRING_AI_ASSISTANT_CHAT_ERROR_TIMEOUT='The request timed out. Try again.' +STRING_AI_ASSISTANT_CHAT_ERROR_UPSTREAM_FAILURE='The assistant service couldn'"'"'t finish this response. Please try again later.' +STRING_AI_ASSISTANT_CHAT_ERROR_TOOL_FAILED='The assistant couldn'"'"'t complete that store action.' +STRING_AI_ASSISTANT_CHAT_ERROR_INVALID_TOOL_CALL='The assistant couldn'"'"'t complete that request.' +STRING_AI_ASSISTANT_CHAT_ERROR_OUTCOME_UNKNOWN='I'"'"'m not sure whether that change was applied. Please verify in your store before trying anything else.' +STRING_AI_ASSISTANT_CHAT_ERROR_CANCELLED='Request stopped.' +STRING_AI_ASSISTANT_CHAT_ERROR_CONFIRMATION_DEFERRED='Confirmation isn'"'"'t available yet. Please try again from the chat.' +STRING_AI_ASSISTANT_CHAT_ERROR_MAX_ITERATIONS='The assistant couldn'"'"'t finish after several steps.' +STRING_AI_ASSISTANT_CHAT_ERROR_UNKNOWN='Something went wrong while the assistant was working.' +STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_TITLE_WITH_NAME_RE='Update\ order\ \#.*\ \(.*\)' +STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_SUMMARY_WITH_NAME_RE='Update\ order\ \#.*\ \(.*\):\ emails\ the\ customer' +STRING_AI_ASSISTANT_CONFIRMATION_ORDER_GUEST_DISPLAY_NAME='Guest' +STRING_AI_ASSISTANT_CONFIRMATION_ORDER_REGISTERED_CUSTOMER_DISPLAY_NAME_RE='Customer\ \#.*' +STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_VARIATION_UPDATE_TITLE_WITH_NAME_RE='Update\ variation\ .*\ \(\#.*\)\ for\ product\ \#.*' +STRING_ERROR_SITE_URL_CERTIFICATE_VALIDITY='We couldn'"'"'t establish a secure connection. Check that your device date and time are correct, then try again.' +STRING_ERROR_SITE_URL_REMOTE_CERTIFICATE='We couldn'"'"'t verify this site'"'"'s security certificate. Contact your hosting provider or site administrator, then try again.' +STRING_WOOPOS_ORDERS_ITEMS_SUBTOTAL_COUNT_PLURAL_ONE_RE='Items\ subtotal\ \(.*\ item\)' +STRING_WOOPOS_ORDERS_ITEMS_SUBTOTAL_COUNT_PLURAL_OTHER_RE='Items\ subtotal\ \(.*\ items\)' + +# Parameterized strings intentionally emit only *_RE variables. +# Do not assert raw % placeholders in flows. +# STRING_PRODUCT_VARIATION_OPTIONS -> STRING_PRODUCT_VARIATION_OPTIONS_RE +# STRING_PRODUCT_VARIATION_MULTIPLE_COUNT -> STRING_PRODUCT_VARIATION_MULTIPLE_COUNT_RE +# STRING_VERSION_WITH_NAME_PARAM -> STRING_VERSION_WITH_NAME_PARAM_RE +# STRING_SELECTION_COUNT -> STRING_SELECTION_COUNT_RE +# STRING_LAST_UPDATE -> STRING_LAST_UPDATE_RE +# STRING_LAST_UPDATE_WITH_FREQUENCY -> STRING_LAST_UPDATE_WITH_FREQUENCY_RE +# STRING_SORTED_BY -> STRING_SORTED_BY_RE +# STRING_ADD -> STRING_ADD_RE +# STRING_LOGIN_NO_JETPACK_USERNAME -> STRING_LOGIN_NO_JETPACK_USERNAME_RE +# STRING_LOGIN_QR_SCANNER_VISIT_URL -> STRING_LOGIN_QR_SCANNER_VISIT_URL_RE +# STRING_LOGIN_QR_MATCH_COUNTDOWN -> STRING_LOGIN_QR_MATCH_COUNTDOWN_RE +# STRING_LOGIN_QR_SCANNER_ERROR_INSTALL_QR_BODY -> STRING_LOGIN_QR_SCANNER_ERROR_INSTALL_QR_BODY_RE +# STRING_LOGIN_VERIFYING_SITE_ERROR -> STRING_LOGIN_VERIFYING_SITE_ERROR_RE +# STRING_LOGIN_WPCOM_ACCOUNT_MISMATCH -> STRING_LOGIN_WPCOM_ACCOUNT_MISMATCH_RE +# STRING_LOGIN_JETPACK_NOT_CONNECTED -> STRING_LOGIN_JETPACK_NOT_CONNECTED_RE +# STRING_LOGIN_NOT_WOO_STORE -> STRING_LOGIN_NOT_WOO_STORE_RE +# STRING_LOGIN_NO_JETPACK -> STRING_LOGIN_NO_JETPACK_RE +# STRING_LOGIN_EMAIL_HELP_DESC -> STRING_LOGIN_EMAIL_HELP_DESC_RE +# STRING_LOGIN_SIMPLE_WPCOM_SITE -> STRING_LOGIN_SIMPLE_WPCOM_SITE_RE +# STRING_LOGIN_JETPACK_INSTALLATION_ENTER_SITE_CREDENTIALS -> STRING_LOGIN_JETPACK_INSTALLATION_ENTER_SITE_CREDENTIALS_RE +# STRING_LOGIN_JETPACK_CONNECTION_ENTER_SITE_CREDENTIALS -> STRING_LOGIN_JETPACK_CONNECTION_ENTER_SITE_CREDENTIALS_RE +# STRING_LOGIN_JETPACK_STEPS_SCREEN_SUBTITLE -> STRING_LOGIN_JETPACK_STEPS_SCREEN_SUBTITLE_RE +# STRING_LOGIN_JETPACK_STEPS_SCREEN_SUBTITLE_DONE -> STRING_LOGIN_JETPACK_STEPS_SCREEN_SUBTITLE_DONE_RE +# STRING_LOGIN_JETPACK_INSTALLATION_ERROR_CODE_TEMPLATE -> STRING_LOGIN_JETPACK_INSTALLATION_ERROR_CODE_TEMPLATE_RE +# STRING_LOGIN_APPLICATION_PASSWORDS_UNAVAILABLE -> STRING_LOGIN_APPLICATION_PASSWORDS_UNAVAILABLE_RE +# STRING_LOGIN_SITE_CREDENTIALS_HTTP_ERROR -> STRING_LOGIN_SITE_CREDENTIALS_HTTP_ERROR_RE +# STRING_LOGIN_WPCOM_CONNECTION_CONSENT -> STRING_LOGIN_WPCOM_CONNECTION_CONSENT_RE +# STRING_SITE_PICKER_SELECT_STORE_LIST_HEADER_WITH_HIDDEN_SITES -> STRING_SITE_PICKER_SELECT_STORE_LIST_HEADER_WITH_HIDDEN_SITES_RE +# STRING_DASHBOARD_TOP_PERFORMERS_NET_SALES -> STRING_DASHBOARD_TOP_PERFORMERS_NET_SALES_RE +# STRING_MY_STORE_CUSTOM_RANGE_GRANULARITY_LABEL -> STRING_MY_STORE_CUSTOM_RANGE_GRANULARITY_LABEL_RE +# STRING_DYNAMIC_DASHBOARD_WIDGET_MENU_ITEM_HIDE -> STRING_DYNAMIC_DASHBOARD_WIDGET_MENU_ITEM_HIDE_RE +# STRING_DASHBOARD_PRODUCT_STOCK_SALES_LAST_30_DAYS -> STRING_DASHBOARD_PRODUCT_STOCK_SALES_LAST_30_DAYS_RE +# STRING_ANALYTICS_SPEND_SUBTITLE_VALUE -> STRING_ANALYTICS_SPEND_SUBTITLE_VALUE_RE +# STRING_ANALYTICS_TOTAL_SALES_SUBTITLE_VALUE -> STRING_ANALYTICS_TOTAL_SALES_SUBTITLE_VALUE_RE +# STRING_ANALYTICS_PRODUCTS_LIST_ITEM_DESCRIPTION -> STRING_ANALYTICS_PRODUCTS_LIST_ITEM_DESCRIPTION_RE +# STRING_ANALYTICS_LIST_ITEM_PRODUCTS_SOLD -> STRING_ANALYTICS_LIST_ITEM_PRODUCTS_SOLD_RE +# STRING_ORDER_CARD_TRANSITION_NAME -> STRING_ORDER_CARD_TRANSITION_NAME_RE +# STRING_ORDERLIST_MARK_COMPLETED_SUCCESS -> STRING_ORDERLIST_MARK_COMPLETED_SUCCESS_RE +# STRING_ORDERLIST_UPDATING_ORDER_ERROR -> STRING_ORDERLIST_UPDATING_ORDER_ERROR_RE +# STRING_ORDERLIST_SELECTION_COUNT -> STRING_ORDERLIST_SELECTION_COUNT_RE +# STRING_ORDERLIST_SELECTION_COUNT_SINGLE -> STRING_ORDERLIST_SELECTION_COUNT_SINGLE_RE +# STRING_ORDERLIST_BULK_UPDATE_MAXIMUM_REACHED -> STRING_ORDERLIST_BULK_UPDATE_MAXIMUM_REACHED_RE +# STRING_ORDERLIST_BULK_UPDATE_RESULT_PARTIAL_SUCCESS -> STRING_ORDERLIST_BULK_UPDATE_RESULT_PARTIAL_SUCCESS_RE +# STRING_SIMPLE_PAYMENTS_TAKE_PAYMENT_BUTTON -> STRING_SIMPLE_PAYMENTS_TAKE_PAYMENT_BUTTON_RE +# STRING_SIMPLE_PAYMENTS_SHARE_PAYMENT_DIALOG_TITLE -> STRING_SIMPLE_PAYMENTS_SHARE_PAYMENT_DIALOG_TITLE_RE +# STRING_CASH_PAYMENTS_TAKE_PAYMENT_TITLE -> STRING_CASH_PAYMENTS_TAKE_PAYMENT_TITLE_RE +# STRING_CASH_PAYMENTS_ORDER_NOTE_TEXT -> STRING_CASH_PAYMENTS_ORDER_NOTE_TEXT_RE +# STRING_CUSTOM_AMOUNTS_PERCENTAGE_LABEL -> STRING_CUSTOM_AMOUNTS_PERCENTAGE_LABEL_RE +# STRING_TAX_NAME_WITH_TAX_PERCENT -> STRING_TAX_NAME_WITH_TAX_PERCENT_RE +# STRING_ORDER_CREATION_COUPON_DISCOUNT_VALUE -> STRING_ORDER_CREATION_COUPON_DISCOUNT_VALUE_RE +# STRING_ORDER_CREATION_DISCOUNT_AMOUNT_WITH_CURRENCY -> STRING_ORDER_CREATION_DISCOUNT_AMOUNT_WITH_CURRENCY_RE +# STRING_ORDER_CREATION_DISCOUNTS_TOTAL_VALUE -> STRING_ORDER_CREATION_DISCOUNTS_TOTAL_VALUE_RE +# STRING_ORDER_CREATION_FEE_PERCENTAGE_CALCULATED_AMOUNT -> STRING_ORDER_CREATION_FEE_PERCENTAGE_CALCULATED_AMOUNT_RE +# STRING_ORDER_CREATION_BARCODE_SCANNING_UNABLE_TO_ADD_PRODUCT -> STRING_ORDER_CREATION_BARCODE_SCANNING_UNABLE_TO_ADD_PRODUCT_RE +# STRING_ORDER_EDITING_CURRENCY_MISMATCH_MESSAGE -> STRING_ORDER_EDITING_CURRENCY_MISMATCH_MESSAGE_RE +# STRING_ORDERFILTERS_SELECTED_FILTER_FALLBACK_DISPLAY_VALUE -> STRING_ORDERFILTERS_SELECTED_FILTER_FALLBACK_DISPLAY_VALUE_RE +# STRING_ORDERFILTERS_FILTERS_COUNT_TITLE -> STRING_ORDERFILTERS_FILTERS_COUNT_TITLE_RE +# STRING_ORDERFILTERS_ORDER_STATUS_WITH_COUNT_FILTER_OPTION -> STRING_ORDERFILTERS_ORDER_STATUS_WITH_COUNT_FILTER_OPTION_RE +# STRING_ORDERDETAIL_CUSTOMER_NOTE -> STRING_ORDERDETAIL_CUSTOMER_NOTE_RE +# STRING_ORDERDETAIL_ORDERSTATUS_ORDERNUM -> STRING_ORDERDETAIL_ORDERSTATUS_ORDERNUM_RE +# STRING_ORDERDETAIL_PRODUCT_LINEITEM_ATTRIBUTES -> STRING_ORDERDETAIL_PRODUCT_LINEITEM_ATTRIBUTES_RE +# STRING_ORDERDETAIL_PRODUCT_LINEITEM_SKU_VALUE -> STRING_ORDERDETAIL_PRODUCT_LINEITEM_SKU_VALUE_RE +# STRING_ORDERDETAIL_PAYMENT_SUMMARY_COMPLETED -> STRING_ORDERDETAIL_PAYMENT_SUMMARY_COMPLETED_RE +# STRING_ORDERDETAIL_PAYMENT_SUMMARY_ONHOLD -> STRING_ORDERDETAIL_PAYMENT_SUMMARY_ONHOLD_RE +# STRING_ORDERDETAIL_REFUNDED_LINE_WITH_INFO -> STRING_ORDERDETAIL_REFUNDED_LINE_WITH_INFO_RE +# STRING_ORDERDETAIL_REFUND_DETAIL -> STRING_ORDERDETAIL_REFUND_DETAIL_RE +# STRING_ORDERDETAIL_DISCOUNT_ITEMS -> STRING_ORDERDETAIL_DISCOUNT_ITEMS_RE +# STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_HEADER -> STRING_ORDERDETAIL_SHIPPING_LABEL_ITEM_HEADER_RE +# STRING_ORDERDETAIL_SHIPPING_LABEL_CARRIER_INFO -> STRING_ORDERDETAIL_SHIPPING_LABEL_CARRIER_INFO_RE +# STRING_ORDERDETAIL_SHIPPING_LABEL_REFUND_TITLE -> STRING_ORDERDETAIL_SHIPPING_LABEL_REFUND_TITLE_RE +# STRING_ORDERDETAIL_SHIPPING_LABEL_REFUND_SUBTITLE -> STRING_ORDERDETAIL_SHIPPING_LABEL_REFUND_SUBTITLE_RE +# STRING_ORDERDETAIL_SHIPPING_LABEL_SHIPMENT_HEADER -> STRING_ORDERDETAIL_SHIPPING_LABEL_SHIPMENT_HEADER_RE +# STRING_ORDERDETAIL_SHIPPING_LABEL_SHIPMENT_ITEMS_ONE -> STRING_ORDERDETAIL_SHIPPING_LABEL_SHIPMENT_ITEMS_ONE_RE +# STRING_ORDERDETAIL_SHIPPING_LABEL_SHIPMENT_ITEMS_MULTIPLE -> STRING_ORDERDETAIL_SHIPPING_LABEL_SHIPMENT_ITEMS_MULTIPLE_RE +# STRING_ORDER_DETAIL_ATTRIBUTION_ORGANIC_ORIGIN -> STRING_ORDER_DETAIL_ATTRIBUTION_ORGANIC_ORIGIN_RE +# STRING_ORDER_DETAIL_ATTRIBUTION_REFERRAL_ORIGIN -> STRING_ORDER_DETAIL_ATTRIBUTION_REFERRAL_ORIGIN_RE +# STRING_ORDER_DETAIL_ATTRIBUTION_UTM_ORIGIN -> STRING_ORDER_DETAIL_ATTRIBUTION_UTM_ORIGIN_RE +# STRING_SHIPPING_LABEL_REFUND_BUTTON -> STRING_SHIPPING_LABEL_REFUND_BUTTON_RE +# STRING_SHIPPING_LABEL_SELECTED_PAYMENT_DESCRIPTION -> STRING_SHIPPING_LABEL_SELECTED_PAYMENT_DESCRIPTION_RE +# STRING_SHIPPING_LABEL_VALIDATION_ERROR_TEMPLATE -> STRING_SHIPPING_LABEL_VALIDATION_ERROR_TEMPLATE_RE +# STRING_SHIPPING_LABEL_PACKAGE_DETAILS_WEIGHT_HINT -> STRING_SHIPPING_LABEL_PACKAGE_DETAILS_WEIGHT_HINT_RE +# STRING_SHIPPING_LABEL_PACKAGE_DETAILS_ITEMS_COUNT_ONE -> STRING_SHIPPING_LABEL_PACKAGE_DETAILS_ITEMS_COUNT_ONE_RE +# STRING_SHIPPING_LABEL_PACKAGE_DETAILS_ITEMS_COUNT_MANY -> STRING_SHIPPING_LABEL_PACKAGE_DETAILS_ITEMS_COUNT_MANY_RE +# STRING_SHIPPING_LABEL_USPS_HAZMAT_INSTRUCTIONS -> STRING_SHIPPING_LABEL_USPS_HAZMAT_INSTRUCTIONS_RE +# STRING_SHIPPING_LABEL_USPS_SEARCH_TOOL -> STRING_SHIPPING_LABEL_USPS_SEARCH_TOOL_RE +# STRING_SHIPPING_LABEL_HAZMAT_CONTENT_DHL_INSTRUCTIONS -> STRING_SHIPPING_LABEL_HAZMAT_CONTENT_DHL_INSTRUCTIONS_RE +# STRING_SHIPPING_LABEL_PACKAGE_DETAILS_TITLE_TEMPLATE -> STRING_SHIPPING_LABEL_PACKAGE_DETAILS_TITLE_TEMPLATE_RE +# STRING_SHIPPING_LABEL_SINGLE_PACKAGE_TOTAL_WEIGHT -> STRING_SHIPPING_LABEL_SINGLE_PACKAGE_TOTAL_WEIGHT_RE +# STRING_SHIPPING_LABEL_MULTI_PACKAGES_ITEMS_COUNT -> STRING_SHIPPING_LABEL_MULTI_PACKAGES_ITEMS_COUNT_RE +# STRING_SHIPPING_LABEL_MULTI_PACKAGES_TOTAL_WEIGHT -> STRING_SHIPPING_LABEL_MULTI_PACKAGES_TOTAL_WEIGHT_RE +# STRING_SHIPPING_LABEL_PACKAGE_DETAILS_ITEMS_WEIGHT_PRICE -> STRING_SHIPPING_LABEL_PACKAGE_DETAILS_ITEMS_WEIGHT_PRICE_RE +# STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_SHIPMENT_COST_BASE_FEE -> STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_SHIPMENT_COST_BASE_FEE_RE +# STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_PURCHASE_LABEL -> STRING_SHIPPING_LABEL_SHIPMENT_DETAILS_PURCHASE_LABEL_RE +# STRING_SHIPPING_LABEL_SELECT_ORIGIN_DEFAULT_ADDRESS -> STRING_SHIPPING_LABEL_SELECT_ORIGIN_DEFAULT_ADDRESS_RE +# STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_LENGTH -> STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_LENGTH_RE +# STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_WIDTH -> STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_WIDTH_RE +# STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_HEIGHT -> STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_HEIGHT_RE +# STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_EMPTY_WEIGHT -> STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_FIELD_EMPTY_WEIGHT_RE +# STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_API_FAILURE -> STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_API_FAILURE_RE +# STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_SUCCESS_MESSAGE -> STRING_SHIPPING_LABEL_CREATE_CUSTOM_PACKAGE_SUCCESS_MESSAGE_RE +# STRING_SHIPPING_LABEL_PAYMENTS_TYPE_DIGITS -> STRING_SHIPPING_LABEL_PAYMENTS_TYPE_DIGITS_RE +# STRING_SHIPPING_LABEL_PAYMENTS_ACCOUNT_INFO -> STRING_SHIPPING_LABEL_PAYMENTS_ACCOUNT_INFO_RE +# STRING_SHIPPING_LABEL_PAYMENTS_EMAIL_RECEIPTS_CHECKBOX -> STRING_SHIPPING_LABEL_PAYMENTS_EMAIL_RECEIPTS_CHECKBOX_RE +# STRING_SHIPPING_LABEL_PAYMENTS_EXPIRATION_DATE -> STRING_SHIPPING_LABEL_PAYMENTS_EXPIRATION_DATE_RE +# STRING_SHIPPING_LABEL_SHIPPING_CARRIER_RATES_DELIVERY_ESTIMATE_ONE -> STRING_SHIPPING_LABEL_SHIPPING_CARRIER_RATES_DELIVERY_ESTIMATE_ONE_RE +# STRING_SHIPPING_LABEL_SHIPPING_CARRIER_RATES_DELIVERY_ESTIMATE_MANY -> STRING_SHIPPING_LABEL_SHIPPING_CARRIER_RATES_DELIVERY_ESTIMATE_MANY_RE +# STRING_SHIPPING_LABEL_SHIPPING_CARRIER_FLAT_FEE_BANNER_MESSAGE -> STRING_SHIPPING_LABEL_SHIPPING_CARRIER_FLAT_FEE_BANNER_MESSAGE_RE +# STRING_SHIPPING_LABEL_SHIPPING_CARRIER_SHIPPING_METHOD_BANNER_MESSAGE -> STRING_SHIPPING_LABEL_SHIPPING_CARRIER_SHIPPING_METHOD_BANNER_MESSAGE_RE +# STRING_SHIPPING_LABEL_RATE_OPTION_SIGNATURE_REQUIRED -> STRING_SHIPPING_LABEL_RATE_OPTION_SIGNATURE_REQUIRED_RE +# STRING_SHIPPING_LABEL_RATE_OPTION_ADULT_SIGNATURE_REQUIRED -> STRING_SHIPPING_LABEL_RATE_OPTION_ADULT_SIGNATURE_REQUIRED_RE +# STRING_SHIPPING_LABEL_RATE_INCLUDED_OPTIONS -> STRING_SHIPPING_LABEL_RATE_INCLUDED_OPTIONS_RE +# STRING_SHIPPING_LABEL_RATE_INSURANCE_UP_TO -> STRING_SHIPPING_LABEL_RATE_INSURANCE_UP_TO_RE +# STRING_SHIPPING_LABEL_RATE_INCLUDED_OPTIONS_INSURANCE -> STRING_SHIPPING_LABEL_RATE_INCLUDED_OPTIONS_INSURANCE_RE +# STRING_SHIPPING_LABEL_SELECTED_RATES_DESCRIPTION -> STRING_SHIPPING_LABEL_SELECTED_RATES_DESCRIPTION_RE +# STRING_SHIPPING_LABEL_SELECTED_RATES_TOTAL_DESCRIPTION -> STRING_SHIPPING_LABEL_SELECTED_RATES_TOTAL_DESCRIPTION_RE +# STRING_SHIPPING_LABEL_CUSTOMS_ITN_REQUIRED_COUNTRY -> STRING_SHIPPING_LABEL_CUSTOMS_ITN_REQUIRED_COUNTRY_RE +# STRING_SHIPPING_LABEL_CUSTOMS_LINE_ITEM -> STRING_SHIPPING_LABEL_CUSTOMS_LINE_ITEM_RE +# STRING_SHIPPING_LABEL_CUSTOMS_LEARN_MORE_ITN -> STRING_SHIPPING_LABEL_CUSTOMS_LEARN_MORE_ITN_RE +# STRING_SHIPPING_LABEL_CUSTOMS_LEARN_MORE_HS_TARIFF_NUMBER -> STRING_SHIPPING_LABEL_CUSTOMS_LEARN_MORE_HS_TARIFF_NUMBER_RE +# STRING_SHIPPING_LABEL_CUSTOMS_VALUE_HINT -> STRING_SHIPPING_LABEL_CUSTOMS_VALUE_HINT_RE +# STRING_SHIPPING_LABEL_CUSTOMS_WEIGHT_HINT -> STRING_SHIPPING_LABEL_CUSTOMS_WEIGHT_HINT_RE +# STRING_SHIPPING_LABEL_MOVE_ITEM_DIALOG_DESCRIPTION -> STRING_SHIPPING_LABEL_MOVE_ITEM_DIALOG_DESCRIPTION_RE +# STRING_ORDER_REFUNDS_TITLE_WITH_AMOUNT -> STRING_ORDER_REFUNDS_TITLE_WITH_AMOUNT_RE +# STRING_ORDER_REFUNDS_REFUNDED_VIA -> STRING_ORDER_REFUNDS_REFUNDED_VIA_RE +# STRING_ORDER_REFUNDS_METHOD -> STRING_ORDER_REFUNDS_METHOD_RE +# STRING_ORDER_REFUNDS_AMOUNT_REFUND_PROGRESS_MESSAGE -> STRING_ORDER_REFUNDS_AMOUNT_REFUND_PROGRESS_MESSAGE_RE +# STRING_ORDER_REFUNDS_ITEMS_SELECTED -> STRING_ORDER_REFUNDS_ITEMS_SELECTED_RE +# STRING_ORDER_REFUNDS_ITEM_DESCRIPTION -> STRING_ORDER_REFUNDS_ITEM_DESCRIPTION_RE +# STRING_ORDER_REFUNDS_DETAIL_ITEM_DESCRIPTION -> STRING_ORDER_REFUNDS_DETAIL_ITEM_DESCRIPTION_RE +# STRING_ORDER_REFUNDS_REFUND_INFO_DESCRIPTION_ONE -> STRING_ORDER_REFUNDS_REFUND_INFO_DESCRIPTION_ONE_RE +# STRING_ORDER_REFUNDS_REFUND_INFO_DESCRIPTION_MANY -> STRING_ORDER_REFUNDS_REFUND_INFO_DESCRIPTION_MANY_RE +# STRING_ORDER_REFUNDS_SHIPPING_REFUND_VARIABLE_NOTICE -> STRING_ORDER_REFUNDS_SHIPPING_REFUND_VARIABLE_NOTICE_RE +# STRING_ORDERSTATUS_CONTENTDESC_WITHSTATUS -> STRING_ORDERSTATUS_CONTENTDESC_WITHSTATUS_RE +# STRING_CARD_READER_HUB_PAYOUT_SUMMARY_FUNDS_AVAILABLE_AFTER_PLURAL -> STRING_CARD_READER_HUB_PAYOUT_SUMMARY_FUNDS_AVAILABLE_AFTER_PLURAL_RE +# STRING_CARD_READER_HUB_PAYOUT_SUMMARY_FUNDS_AVAILABLE_AFTER_ONE -> STRING_CARD_READER_HUB_PAYOUT_SUMMARY_FUNDS_AVAILABLE_AFTER_ONE_RE +# STRING_CARD_READER_HUB_PAYOUT_SUMMARY_AVAILABLE_PAYOUT_TIME_WEEKLY -> STRING_CARD_READER_HUB_PAYOUT_SUMMARY_AVAILABLE_PAYOUT_TIME_WEEKLY_RE +# STRING_CARD_READER_HUB_PAYOUT_SUMMARY_AVAILABLE_PAYOUT_TIME_MONTHLY -> STRING_CARD_READER_HUB_PAYOUT_SUMMARY_AVAILABLE_PAYOUT_TIME_MONTHLY_RE +# STRING_CARD_READER_TAP_TO_PAY_EXPLANATION_TRY_AND_REFUND_WITH_AMOUNT -> STRING_CARD_READER_TAP_TO_PAY_EXPLANATION_TRY_AND_REFUND_WITH_AMOUNT_RE +# STRING_CARD_READER_TAP_TO_PAY_ABOUT_IMPORTANT_INFO_DESCRIPTION_1 -> STRING_CARD_READER_TAP_TO_PAY_ABOUT_IMPORTANT_INFO_DESCRIPTION_1_RE +# STRING_CARD_READER_PAYMENT_FAILED_AMOUNT_TOO_SMALL -> STRING_CARD_READER_PAYMENT_FAILED_AMOUNT_TOO_SMALL_RE +# STRING_CARD_READER_MODE_READY_TO_PAIR_STORE -> STRING_CARD_READER_MODE_READY_TO_PAIR_STORE_RE +# STRING_CARD_READER_PAYMENT_DESCRIPTION_V2 -> STRING_CARD_READER_PAYMENT_DESCRIPTION_V2_RE +# STRING_CARD_READER_PAYMENT_RECEIPT_EMAIL_SUBJECT -> STRING_CARD_READER_PAYMENT_RECEIPT_EMAIL_SUBJECT_RE +# STRING_CARD_READER_PAYMENT_READER_RECEIPT_SENT -> STRING_CARD_READER_PAYMENT_READER_RECEIPT_SENT_RE +# STRING_CARD_READER_CONNECT_READER_FOUND_HEADER -> STRING_CARD_READER_CONNECT_READER_FOUND_HEADER_RE +# STRING_CARD_READER_DETAIL_CONNECTED_BATTERY_PERCENTAGE -> STRING_CARD_READER_DETAIL_CONNECTED_BATTERY_PERCENTAGE_RE +# STRING_CARD_READER_DETAIL_CONNECTED_FIRMWARE_VERSION -> STRING_CARD_READER_DETAIL_CONNECTED_FIRMWARE_VERSION_RE +# STRING_CARD_READER_SOFTWARE_UPDATE_PROGRESS_DESCRIPTION_LOW_BATTERY -> STRING_CARD_READER_SOFTWARE_UPDATE_PROGRESS_DESCRIPTION_LOW_BATTERY_RE +# STRING_CARD_READER_SOFTWARE_UPDATE_PROGRESS_INDICATOR -> STRING_CARD_READER_SOFTWARE_UPDATE_PROGRESS_INDICATOR_RE +# STRING_CARD_READER_ONBOARDING_COUNTRY_NOT_SUPPORTED_HEADER -> STRING_CARD_READER_ONBOARDING_COUNTRY_NOT_SUPPORTED_HEADER_RE +# STRING_CARD_READER_ONBOARDING_STRIPE_UNSUPPORTED_IN_COUNTRY_HEADER -> STRING_CARD_READER_ONBOARDING_STRIPE_UNSUPPORTED_IN_COUNTRY_HEADER_RE +# STRING_CARD_READER_ONBOARDING_WCPAY_UNSUPPORTED_IN_COUNTRY_HEADER -> STRING_CARD_READER_ONBOARDING_WCPAY_UNSUPPORTED_IN_COUNTRY_HEADER_RE +# STRING_CARD_READER_ONBOARDING_STRIPE_ACCOUNT_IN_UNSUPPORTED_COUNTRY -> STRING_CARD_READER_ONBOARDING_STRIPE_ACCOUNT_IN_UNSUPPORTED_COUNTRY_RE +# STRING_CARD_READER_ONBOARDING_ACCOUNT_PENDING_REQUIREMENTS_HINT -> STRING_CARD_READER_ONBOARDING_ACCOUNT_PENDING_REQUIREMENTS_HINT_RE +# STRING_NEW_NOTIFICATIONS -> STRING_NEW_NOTIFICATIONS_RE +# STRING_REVIEW_CARD_TRANSITION_NAME -> STRING_REVIEW_CARD_TRANSITION_NAME_RE +# STRING_REVIEW_LIST_ITEM_TITLE -> STRING_REVIEW_LIST_ITEM_TITLE_RE +# STRING_PRODUCT_REVIEW_LIST_ITEM_TITLE -> STRING_PRODUCT_REVIEW_LIST_ITEM_TITLE_RE +# STRING_REVIEW_MODERATION_UNDO -> STRING_REVIEW_MODERATION_UNDO_RE +# STRING_PRODUCT_SALE_DATE_FROM_TO -> STRING_PRODUCT_SALE_DATE_FROM_TO_RE +# STRING_PRODUCT_SALE_DATE_FROM -> STRING_PRODUCT_SALE_DATE_FROM_RE +# STRING_PRODUCT_SALE_DATE_TO -> STRING_PRODUCT_SALE_DATE_TO_RE +# STRING_PRODUCT_RATINGS_COUNT -> STRING_PRODUCT_RATINGS_COUNT_RE +# STRING_PRODUCT_STOCK_STATUS_INSTOCK_QUANTIFIED -> STRING_PRODUCT_STOCK_STATUS_INSTOCK_QUANTIFIED_RE +# STRING_PRODUCT_STOCK_STATUS_INSTOCK_WITH_VARIATIONS -> STRING_PRODUCT_STOCK_STATUS_INSTOCK_WITH_VARIATIONS_RE +# STRING_PRODUCT_STOCK_COUNT -> STRING_PRODUCT_STOCK_COUNT_RE +# STRING_PRODUCT_LIST_FILTERS_COUNT -> STRING_PRODUCT_LIST_FILTERS_COUNT_RE +# STRING_PRODUCT_LIST_FILTERS_SELECTED -> STRING_PRODUCT_LIST_FILTERS_SELECTED_RE +# STRING_PRODUCT_LIST_UNSAVED_PRODUCT_UNSELECTED_TITLE -> STRING_PRODUCT_LIST_UNSAVED_PRODUCT_UNSELECTED_TITLE_RE +# STRING_PRODUCT_DESCRIPTION_HINT_WITH_TITLE -> STRING_PRODUCT_DESCRIPTION_HINT_WITH_TITLE_RE +# STRING_PRODUCT_DETAIL_PRODUCT_TYPE_HINT -> STRING_PRODUCT_DETAIL_PRODUCT_TYPE_HINT_RE +# STRING_PRODUCT_DUPLICATE_COPIED_PRODUCT_NAME -> STRING_PRODUCT_DUPLICATE_COPIED_PRODUCT_NAME_RE +# STRING_PRODUCT_SELECTION_COUNT -> STRING_PRODUCT_SELECTION_COUNT_RE +# STRING_PRODUCT_SELECTION_COUNT_SINGLE -> STRING_PRODUCT_SELECTION_COUNT_SINGLE_RE +# STRING_PRODUCT_COUNT_ONE -> STRING_PRODUCT_COUNT_ONE_RE +# STRING_PRODUCT_COUNT_MANY -> STRING_PRODUCT_COUNT_MANY_RE +# STRING_CATEGORY_COUNT_ONE -> STRING_CATEGORY_COUNT_ONE_RE +# STRING_CATEGORY_COUNT_MANY -> STRING_CATEGORY_COUNT_MANY_RE +# STRING_CROSS_SELL_PRODUCT_COUNT_ONE -> STRING_CROSS_SELL_PRODUCT_COUNT_ONE_RE +# STRING_CROSS_SELL_PRODUCT_COUNT_MANY -> STRING_CROSS_SELL_PRODUCT_COUNT_MANY_RE +# STRING_UPSELL_PRODUCT_COUNT_ONE -> STRING_UPSELL_PRODUCT_COUNT_ONE_RE +# STRING_UPSELL_PRODUCT_COUNT_MANY -> STRING_UPSELL_PRODUCT_COUNT_MANY_RE +# STRING_PRODUCT_CATEGORY_SELECTOR_SELECT_BUTTON_TITLE_DEFAULT -> STRING_PRODUCT_CATEGORY_SELECTOR_SELECT_BUTTON_TITLE_DEFAULT_RE +# STRING_PRODUCT_CREATION_AI_PREVIEW_VARIANT_SELECTOR -> STRING_PRODUCT_CREATION_AI_PREVIEW_VARIANT_SELECTOR_RE +# STRING_VARIATIONS_BULK_UPDATE_PRICE_INFO -> STRING_VARIATIONS_BULK_UPDATE_PRICE_INFO_RE +# STRING_VARIATIONS_BULK_UPDATE_CURRENT_PRICE -> STRING_VARIATIONS_BULK_UPDATE_CURRENT_PRICE_RE +# STRING_VARIATIONS_BULK_UPDATE_STOCK_QUANTITY_INFO -> STRING_VARIATIONS_BULK_UPDATE_STOCK_QUANTITY_INFO_RE +# STRING_VARIATIONS_BULK_UPDATE_CURRENT_STOCK_QUANTITY -> STRING_VARIATIONS_BULK_UPDATE_CURRENT_STOCK_QUANTITY_RE +# STRING_VARIATIONS_BULK_CREATION_WARNING_MESSAGE -> STRING_VARIATIONS_BULK_CREATION_WARNING_MESSAGE_RE +# STRING_VARIATIONS_BULK_CREATION_CONFIRMATION_MESSAGE -> STRING_VARIATIONS_BULK_CREATION_CONFIRMATION_MESSAGE_RE +# STRING_PRODUCT_IMAGES_UPLOADING_MULTI_NOTIF_MESSAGE -> STRING_PRODUCT_IMAGES_UPLOADING_MULTI_NOTIF_MESSAGE_RE +# STRING_PRODUCT_IMAGE_SERVICE_ERROR_UPLOADING_SINGLE -> STRING_PRODUCT_IMAGE_SERVICE_ERROR_UPLOADING_SINGLE_RE +# STRING_PRODUCT_IMAGE_SERVICE_ERROR_UPLOADING_MULTIPLE -> STRING_PRODUCT_IMAGE_SERVICE_ERROR_UPLOADING_MULTIPLE_RE +# STRING_PRODUCT_IMAGES_ERROR_DETAIL_TITLE -> STRING_PRODUCT_IMAGES_ERROR_DETAIL_TITLE_RE +# STRING_PRODUCT_UPDATE_NOTIFICATION -> STRING_PRODUCT_UPDATE_NOTIFICATION_RE +# STRING_PRODUCT_UPDATE_SUCCESS_NOTIFICATION_CONTENT -> STRING_PRODUCT_UPDATE_SUCCESS_NOTIFICATION_CONTENT_RE +# STRING_PRODUCT_UPDATE_FAILURE_NOTIFICATION -> STRING_PRODUCT_UPDATE_FAILURE_NOTIFICATION_RE +# STRING_EMPTY_MESSAGE_WITH_SEARCH -> STRING_EMPTY_MESSAGE_WITH_SEARCH_RE +# STRING_SETTINGS_CONFIRM_LOGOUT -> STRING_SETTINGS_CONFIRM_LOGOUT_RE +# STRING_SETTINGS_FOOTER -> STRING_SETTINGS_FOOTER_RE +# STRING_SETTINGS_NOTIFS_NEW_ORDERS_HIGH_VALUE_SUBTITLE -> STRING_SETTINGS_NOTIFS_NEW_ORDERS_HIGH_VALUE_SUBTITLE_RE +# STRING_SETTINGS_NOTIFS_NEW_ORDERS_THRESHOLD -> STRING_SETTINGS_NOTIFS_NEW_ORDERS_THRESHOLD_RE +# STRING_SETTINGS_NOTIFS_STOCK_TWO_SUBTITLE -> STRING_SETTINGS_NOTIFS_STOCK_TWO_SUBTITLE_RE +# STRING_SETTINGS_NOTIFS_STOCK_LOW_STOCK_THRESHOLD -> STRING_SETTINGS_NOTIFS_STOCK_LOW_STOCK_THRESHOLD_RE +# STRING_SETTINGS_NOTIFS_NEW_REVIEWS_SELECTED_RATING -> STRING_SETTINGS_NOTIFS_NEW_REVIEWS_SELECTED_RATING_RE +# STRING_SETTINGS_NOTIFS_NEW_REVIEWS_SELECTED_RATING_ONE -> STRING_SETTINGS_NOTIFS_NEW_REVIEWS_SELECTED_RATING_ONE_RE +# STRING_SETTINGS_ABOUT_RECOMMEND_APP_MESSAGE -> STRING_SETTINGS_ABOUT_RECOMMEND_APP_MESSAGE_RE +# STRING_PLUGIN_STATE_UPDATE_AVAILABLE -> STRING_PLUGIN_STATE_UPDATE_AVAILABLE_RE +# STRING_ENTER_VERIFICATION_CODE_SMS -> STRING_ENTER_VERIFICATION_CODE_SMS_RE +# STRING_LOGIN_ERROR_WHILE_ADDING_SITE -> STRING_LOGIN_ERROR_WHILE_ADDING_SITE_RE +# STRING_ENTER_EMAIL_FOR_SITE -> STRING_ENTER_EMAIL_FOR_SITE_RE +# STRING_ENTER_CREDENTIALS_FOR_SITE -> STRING_ENTER_CREDENTIALS_FOR_SITE_RE +# STRING_LOGIN_SITE_CREDENTIALS_MAGIC_LINK_LABEL -> STRING_LOGIN_SITE_CREDENTIALS_MAGIC_LINK_LABEL_RE +# STRING_FEEDBACK_COMPLETED_DESCRIPTION -> STRING_FEEDBACK_COMPLETED_DESCRIPTION_RE +# STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_BODY_CONNECT -> STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_BODY_CONNECT_RE +# STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_BODY_SETUP -> STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_BODY_SETUP_RE +# STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_ERROR_PLUGIN_UPDATE_REQUIRED -> STRING_WOO_PUSH_NOTIFICATIONS_CONNECTION_STEPS_ERROR_PLUGIN_UPDATE_REQUIRED_RE +# STRING_JETPACK_INSTALL_START_SUBTITLE -> STRING_JETPACK_INSTALL_START_SUBTITLE_RE +# STRING_JETPACK_INSTALL_PROGRESS_SUBTITLE -> STRING_JETPACK_INSTALL_PROGRESS_SUBTITLE_RE +# STRING_JETPACK_INSTALL_PROGRESS_FAILED_TITLE -> STRING_JETPACK_INSTALL_PROGRESS_FAILED_TITLE_RE +# STRING_JETPACK_INSTALL_PROGRESS_FAILED_ALTERNATIVE -> STRING_JETPACK_INSTALL_PROGRESS_FAILED_ALTERNATIVE_RE +# STRING_JETPACK_INSTALL_PROGRESS_FAILED_OPTION_WP_ADMIN -> STRING_JETPACK_INSTALL_PROGRESS_FAILED_OPTION_WP_ADMIN_RE +# STRING_INBOX_NOTE_RECENCY_MINUTES -> STRING_INBOX_NOTE_RECENCY_MINUTES_RE +# STRING_INBOX_NOTE_RECENCY_HOURS -> STRING_INBOX_NOTE_RECENCY_HOURS_RE +# STRING_INBOX_NOTE_RECENCY_DAYS -> STRING_INBOX_NOTE_RECENCY_DAYS_RE +# STRING_INBOX_NOTE_RECENCY_DATE_TIME -> STRING_INBOX_NOTE_RECENCY_DATE_TIME_RE +# STRING_COUPON_LIST_ITEM_LABEL_PRODUCTS_AND_CATEGORIES -> STRING_COUPON_LIST_ITEM_LABEL_PRODUCTS_AND_CATEGORIES_RE +# STRING_COUPON_LIST_ITEM_LABEL_INCLUDED_AND_EXCLUDED -> STRING_COUPON_LIST_ITEM_LABEL_INCLUDED_AND_EXCLUDED_RE +# STRING_COUPON_DETAILS_MINIMUM_SPEND -> STRING_COUPON_DETAILS_MINIMUM_SPEND_RE +# STRING_COUPON_DETAILS_MAXIMUM_SPEND -> STRING_COUPON_DETAILS_MAXIMUM_SPEND_RE +# STRING_COUPON_TYPE_CUSTOM -> STRING_COUPON_TYPE_CUSTOM_RE +# STRING_COUPON_SUMMARY_TEMPLATE -> STRING_COUPON_SUMMARY_TEMPLATE_RE +# STRING_COUPON_DETAILS_EXPIRATION_DATE -> STRING_COUPON_DETAILS_EXPIRATION_DATE_RE +# STRING_COUPON_DETAILS_SHARE_COUPON_ALL -> STRING_COUPON_DETAILS_SHARE_COUPON_ALL_RE +# STRING_COUPON_DETAILS_SHARE_COUPON_SOME -> STRING_COUPON_DETAILS_SHARE_COUPON_SOME_RE +# STRING_COUPON_DETAILS_USAGE_LIMIT_PER_USER_MULTIPLE -> STRING_COUPON_DETAILS_USAGE_LIMIT_PER_USER_MULTIPLE_RE +# STRING_COUPON_DETAILS_USAGE_LIMIT_PER_USER_SINGLE -> STRING_COUPON_DETAILS_USAGE_LIMIT_PER_USER_SINGLE_RE +# STRING_COUPON_DETAILS_USAGE_LIMIT_PER_COUPON_MULTIPLE -> STRING_COUPON_DETAILS_USAGE_LIMIT_PER_COUPON_MULTIPLE_RE +# STRING_COUPON_DETAILS_USAGE_LIMIT_PER_COUPON_SINGLE -> STRING_COUPON_DETAILS_USAGE_LIMIT_PER_COUPON_SINGLE_RE +# STRING_COUPON_DETAILS_USAGE_LIMIT_PER_ITEMS_MULTIPLE -> STRING_COUPON_DETAILS_USAGE_LIMIT_PER_ITEMS_MULTIPLE_RE +# STRING_COUPON_DETAILS_USAGE_LIMIT_PER_ITEMS_SINGLE -> STRING_COUPON_DETAILS_USAGE_LIMIT_PER_ITEMS_SINGLE_RE +# STRING_COUPON_DETAILS_RESTRICTED_EMAILS -> STRING_COUPON_DETAILS_RESTRICTED_EMAILS_RE +# STRING_COUPON_EDIT_AMOUNT_HINT -> STRING_COUPON_EDIT_AMOUNT_HINT_RE +# STRING_COUPON_EDIT_EDIT_PRODUCTS_TITLE -> STRING_COUPON_EDIT_EDIT_PRODUCTS_TITLE_RE +# STRING_COUPON_RESTRICTIONS_MINIMUM_SPEND_HINT -> STRING_COUPON_RESTRICTIONS_MINIMUM_SPEND_HINT_RE +# STRING_COUPON_RESTRICTIONS_MAXIMUM_SPEND_HINT -> STRING_COUPON_RESTRICTIONS_MAXIMUM_SPEND_HINT_RE +# STRING_COUPON_CONDITIONS_PRODUCTS_EDIT_PRODUCTS_TITLE -> STRING_COUPON_CONDITIONS_PRODUCTS_EDIT_PRODUCTS_TITLE_RE +# STRING_PRODUCT_SELECTOR_SELECT_PRODUCT_LABEL -> STRING_PRODUCT_SELECTOR_SELECT_PRODUCT_LABEL_RE +# STRING_PRODUCT_SELECTOR_SELECT_VARIATION_LABEL -> STRING_PRODUCT_SELECTOR_SELECT_VARIATION_LABEL_RE +# STRING_PRODUCT_SELECTOR_SKU_VALUE -> STRING_PRODUCT_SELECTOR_SKU_VALUE_RE +# STRING_PRODUCT_SELECTOR_SELECT_BUTTON_TITLE_DEFAULT -> STRING_PRODUCT_SELECTOR_SELECT_BUTTON_TITLE_DEFAULT_RE +# STRING_PRODUCT_SELECTOR_SELECT_BUTTON_TITLE_ONE -> STRING_PRODUCT_SELECTOR_SELECT_BUTTON_TITLE_ONE_RE +# STRING_PRODUCT_SELECTOR_FILTER_BUTTON_TITLE_DEFAULT -> STRING_PRODUCT_SELECTOR_FILTER_BUTTON_TITLE_DEFAULT_RE +# STRING_ENTER_ACCOUNT_INFO_FOR_SITE -> STRING_ENTER_ACCOUNT_INFO_FOR_SITE_RE +# STRING_CONTINUE_TERMS_OF_SERVICE_TEXT -> STRING_CONTINUE_TERMS_OF_SERVICE_TEXT_RE +# STRING_CONTINUE_WITH_GOOGLE_TERMS_OF_SERVICE_TEXT -> STRING_CONTINUE_WITH_GOOGLE_TERMS_OF_SERVICE_TEXT_RE +# STRING_PRODUCT_DOWNLOADABLE_FILES_VALUE_MULTIPLE -> STRING_PRODUCT_DOWNLOADABLE_FILES_VALUE_MULTIPLE_RE +# STRING_SHIPPING_LABEL_PAYMENTS_CANT_EDIT_WARNING -> STRING_SHIPPING_LABEL_PAYMENTS_CANT_EDIT_WARNING_RE +# STRING_STATS_WIDGET_LAST_UPDATED_MESSAGE -> STRING_STATS_WIDGET_LAST_UPDATED_MESSAGE_RE +# STRING_THEME_PICKER_CAROUSEL_ERROR_PLACEHOLDER_MESSAGE -> STRING_THEME_PICKER_CAROUSEL_ERROR_PLACEHOLDER_MESSAGE_RE +# STRING_THEME_PREVIEW_ACTIVATE_THEME_BUTTON_SETTINGS -> STRING_THEME_PREVIEW_ACTIVATE_THEME_BUTTON_SETTINGS_RE +# STRING_STORE_ONBOARDING_COMPLETED_TASKS_STATUS -> STRING_STORE_ONBOARDING_COMPLETED_TASKS_STATUS_RE +# STRING_STORE_ONBOARDING_COMPLETED_TASKS_FULL_SCREEN_STATUS -> STRING_STORE_ONBOARDING_COMPLETED_TASKS_FULL_SCREEN_STATUS_RE +# STRING_STORE_ONBOARDING_TASK_VIEW_ALL -> STRING_STORE_ONBOARDING_TASK_VIEW_ALL_RE +# STRING_FREE_TRIAL_DAYS_LEFT -> STRING_FREE_TRIAL_DAYS_LEFT_RE +# STRING_FREE_TRIAL_DAYS_LEFT_PLURAL -> STRING_FREE_TRIAL_DAYS_LEFT_PLURAL_RE +# STRING_UPGRADES_CURRENT_PLAN -> STRING_UPGRADES_CURRENT_PLAN_RE +# STRING_UPGRADES_UPGRADEABLE_CAPTION -> STRING_UPGRADES_UPGRADEABLE_CAPTION_RE +# STRING_UPGRADES_TRIAL_ENDED_CAPTION -> STRING_UPGRADES_TRIAL_ENDED_CAPTION_RE +# STRING_UPGRADES_NON_UPGRADEABLE_CAPTION -> STRING_UPGRADES_NON_UPGRADEABLE_CAPTION_RE +# STRING_UPGRADES_PLAN_ENDED_NAME -> STRING_UPGRADES_PLAN_ENDED_NAME_RE +# STRING_SUBSCRIPTION_ID -> STRING_SUBSCRIPTION_ID_RE +# STRING_SUBSCRIPTION_PERIOD_INTERVAL_SINGLE -> STRING_SUBSCRIPTION_PERIOD_INTERVAL_SINGLE_RE +# STRING_SUBSCRIPTION_PERIOD_INTERVAL_MULTIPLE -> STRING_SUBSCRIPTION_PERIOD_INTERVAL_MULTIPLE_RE +# STRING_PRODUCT_SUBSCRIPTION_DESCRIPTION -> STRING_PRODUCT_SUBSCRIPTION_DESCRIPTION_RE +# STRING_PRODUCT_BUNDLE_MULTIPLE_COUNT -> STRING_PRODUCT_BUNDLE_MULTIPLE_COUNT_RE +# STRING_AI_ASSISTANT_VARIATION_CARD_ID_TITLE -> STRING_AI_ASSISTANT_VARIATION_CARD_ID_TITLE_RE +# STRING_PRODUCT_COMPONENT_MULTIPLE_COUNT -> STRING_PRODUCT_COMPONENT_MULTIPLE_COUNT_RE +# STRING_BLAZE_CAMPAIGN_STATUS_CTR_VALUE_SHORTENED -> STRING_BLAZE_CAMPAIGN_STATUS_CTR_VALUE_SHORTENED_RE +# STRING_BLAZE_CAMPAIGN_PREVIEW_DAYS_DURATION -> STRING_BLAZE_CAMPAIGN_PREVIEW_DAYS_DURATION_RE +# STRING_BLAZE_CAMPAIGN_PREVIEW_DAYS_DURATION_ENDLESS -> STRING_BLAZE_CAMPAIGN_PREVIEW_DAYS_DURATION_ENDLESS_RE +# STRING_BLAZE_CAMPAIGN_PREVIEW_TOS_CHECKBOX_LESS_THAN_7_DAYS_CAMPAIGN -> STRING_BLAZE_CAMPAIGN_PREVIEW_TOS_CHECKBOX_LESS_THAN_7_DAYS_CAMPAIGN_RE +# STRING_BLAZE_CAMPAIGN_PREVIEW_TOS_CHECKBOX_OVER_7_DAYS_CAMPAIGN -> STRING_BLAZE_CAMPAIGN_PREVIEW_TOS_CHECKBOX_OVER_7_DAYS_CAMPAIGN_RE +# STRING_BLAZE_CAMPAIGN_PREVIEW_TOS_CHECKBOX_EVERGREEN_CAMPAIGNS -> STRING_BLAZE_CAMPAIGN_PREVIEW_TOS_CHECKBOX_EVERGREEN_CAMPAIGNS_RE +# STRING_BLAZE_CAMPAIGN_OBJECTIVE_SELECT_OBJECTIVE_LABEL -> STRING_BLAZE_CAMPAIGN_OBJECTIVE_SELECT_OBJECTIVE_LABEL_RE +# STRING_BLAZE_CAMPAIGN_OBJECTIVE_GOOD_FOR -> STRING_BLAZE_CAMPAIGN_OBJECTIVE_GOOD_FOR_RE +# STRING_BLAZE_CAMPAIGN_BUDGET_DAYS_DURATION -> STRING_BLAZE_CAMPAIGN_BUDGET_DAYS_DURATION_RE +# STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_ENDLESS_CAMPAIGN_VALUE -> STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_ENDLESS_CAMPAIGN_VALUE_RE +# STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_BOTTOM_SHEET_DURATION -> STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_BOTTOM_SHEET_DURATION_RE +# STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_BOTTOM_SHEET_END_DATE -> STRING_BLAZE_CAMPAIGN_BUDGET_DURATION_BOTTOM_SHEET_END_DATE_RE +# STRING_BLAZE_CAMPAIGN_BUDGET_WEEKLY_SPENDING -> STRING_BLAZE_CAMPAIGN_BUDGET_WEEKLY_SPENDING_RE +# STRING_BLAZE_CAMPAIGN_EDIT_AD_CHARACTERS_REMAINING -> STRING_BLAZE_CAMPAIGN_EDIT_AD_CHARACTERS_REMAINING_RE +# STRING_BLAZE_CAMPAIGN_PAYMENT_LIST_HINT -> STRING_BLAZE_CAMPAIGN_PAYMENT_LIST_HINT_RE +# STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_DESTINATION_WITH_PARAMETERS -> STRING_BLAZE_CAMPAIGN_EDIT_AD_DESTINATION_DESTINATION_WITH_PARAMETERS_RE +# STRING_TAX_RATES_INFO_DIALOG_TAX_BASED_ON_STORE_ADDRESS -> STRING_TAX_RATES_INFO_DIALOG_TAX_BASED_ON_STORE_ADDRESS_RE +# STRING_TAX_RATES_INFO_DIALOG_TAX_BASED_ON_BILLING_ADDRESS -> STRING_TAX_RATES_INFO_DIALOG_TAX_BASED_ON_BILLING_ADDRESS_RE +# STRING_TAX_RATES_INFO_DIALOG_TAX_BASED_ON_SHIPPING_ADDRESS -> STRING_TAX_RATES_INFO_DIALOG_TAX_BASED_ON_SHIPPING_ADDRESS_RE +# STRING_SCAN_TO_UPDATE_INVENTORY_UNABLE_TO_FIND_PRODUCT -> STRING_SCAN_TO_UPDATE_INVENTORY_UNABLE_TO_FIND_PRODUCT_RE +# STRING_SCAN_TO_UPDATE_INVENTORY_SUCCESS_SNACKBAR -> STRING_SCAN_TO_UPDATE_INVENTORY_SUCCESS_SNACKBAR_RE +# STRING_DEFAULT_PRODUCT_TITLE -> STRING_DEFAULT_PRODUCT_TITLE_RE +# STRING_CONFIGURATION_QUANTITY_ITEM -> STRING_CONFIGURATION_QUANTITY_ITEM_RE +# STRING_CONFIGURATION_QUANTITY_ITEM_PLURAL -> STRING_CONFIGURATION_QUANTITY_ITEM_PLURAL_RE +# STRING_CONFIGURATION_QUANTITY_BETWEEN -> STRING_CONFIGURATION_QUANTITY_BETWEEN_RE +# STRING_CONFIGURATION_QUANTITY_LESS_THAN -> STRING_CONFIGURATION_QUANTITY_LESS_THAN_RE +# STRING_CONFIGURATION_QUANTITY_MORE_THAN -> STRING_CONFIGURATION_QUANTITY_MORE_THAN_RE +# STRING_CONFIGURATION_QUANTITY_MORE_THAN_PLURAL -> STRING_CONFIGURATION_QUANTITY_MORE_THAN_PLURAL_RE +# STRING_CONFIGURATION_QUANTITY_RULE_ISSUE -> STRING_CONFIGURATION_QUANTITY_RULE_ISSUE_RE +# STRING_CONFIGURATION_CHILDREN_ISSUE -> STRING_CONFIGURATION_CHILDREN_ISSUE_RE +# STRING_ORDER_CONFIGURATION_PRODUCT_SELECTION -> STRING_ORDER_CONFIGURATION_PRODUCT_SELECTION_RE +# STRING_PRODUCT_UPDATE_STOCK_STATUS_CURRENT_STATUS_SINGLE -> STRING_PRODUCT_UPDATE_STOCK_STATUS_CURRENT_STATUS_SINGLE_RE +# STRING_PRODUCT_UPDATE_STOCK_STATUS_UPDATE_COUNT -> STRING_PRODUCT_UPDATE_STOCK_STATUS_UPDATE_COUNT_RE +# STRING_PRODUCT_UPDATE_STOCK_STATUS_IGNORED_COUNT -> STRING_PRODUCT_UPDATE_STOCK_STATUS_IGNORED_COUNT_RE +# STRING_PRODUCT_UPDATE_STOCK_STATUS_VARIABLE_IGNORED_COUNT -> STRING_PRODUCT_UPDATE_STOCK_STATUS_VARIABLE_IGNORED_COUNT_RE +# STRING_WOOPOS_HOME_SYNCING_CATALOG_PROGRESS -> STRING_WOOPOS_HOME_SYNCING_CATALOG_PROGRESS_RE +# STRING_WOOPOS_REMOVE_ITEM_BUTTON_FROM_CART_CONTENT_DESCRIPTION -> STRING_WOOPOS_REMOVE_ITEM_BUTTON_FROM_CART_CONTENT_DESCRIPTION_RE +# STRING_WOOPOS_PRODUCT_ITEM_CONTENT_DESCRIPTION -> STRING_WOOPOS_PRODUCT_ITEM_CONTENT_DESCRIPTION_RE +# STRING_WOOPOS_VARIABLE_PRODUCT_ITEM_CONTENT_DESCRIPTION_V2 -> STRING_WOOPOS_VARIABLE_PRODUCT_ITEM_CONTENT_DESCRIPTION_V2_RE +# STRING_WOOPOS_VARIATION_ITEM_CONTENT_DESCRIPTION -> STRING_WOOPOS_VARIATION_ITEM_CONTENT_DESCRIPTION_RE +# STRING_WOOPOS_CART_ITEM_PRODUCT_CONTENT_DESCRIPTION -> STRING_WOOPOS_CART_ITEM_PRODUCT_CONTENT_DESCRIPTION_RE +# STRING_WOOPOS_CART_ITEM_LOADING_CONTENT_DESCRIPTION -> STRING_WOOPOS_CART_ITEM_LOADING_CONTENT_DESCRIPTION_RE +# STRING_WOOPOS_CART_ITEM_ERROR_CONTENT_DESCRIPTION -> STRING_WOOPOS_CART_ITEM_ERROR_CONTENT_DESCRIPTION_RE +# STRING_WOOPOS_COUPON_ITEM_CONTENT_DESCRIPTION_V2 -> STRING_WOOPOS_COUPON_ITEM_CONTENT_DESCRIPTION_V2_RE +# STRING_WOOPOS_COUPON_ITEM_EXPIRED_LABEL -> STRING_WOOPOS_COUPON_ITEM_EXPIRED_LABEL_RE +# STRING_WOOPOS_CART_ITEM_COUPON_CONTENT_DESCRIPTION -> STRING_WOOPOS_CART_ITEM_COUPON_CONTENT_DESCRIPTION_RE +# STRING_WOOPOS_CART_ITEM_CUSTOM_AMOUNT_CONTENT_DESCRIPTION -> STRING_WOOPOS_CART_ITEM_CUSTOM_AMOUNT_CONTENT_DESCRIPTION_RE +# STRING_WOOPOS_CART_CUSTOM_AMOUNT_EDIT_CONTENT_DESCRIPTION -> STRING_WOOPOS_CART_CUSTOM_AMOUNT_EDIT_CONTENT_DESCRIPTION_RE +# STRING_WOOPOS_CART_BARCODE_SCAN_RESULT_UNSUPPORTED_PRODUCT -> STRING_WOOPOS_CART_BARCODE_SCAN_RESULT_UNSUPPORTED_PRODUCT_RE +# STRING_WOOPOS_CART_BARCODE_SCAN_RESULT_SERVER_ERROR -> STRING_WOOPOS_CART_BARCODE_SCAN_RESULT_SERVER_ERROR_RE +# STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_SIZE_FORMAT -> STRING_WOOPOS_SETTINGS_LOCAL_CATALOG_SIZE_FORMAT_RE +# STRING_WOOPOS_DATE_TODAY_AT -> STRING_WOOPOS_DATE_TODAY_AT_RE +# STRING_WOOPOS_DATE_YESTERDAY_AT -> STRING_WOOPOS_DATE_YESTERDAY_AT_RE +# STRING_WOOPOS_ORDER_TITLE -> STRING_WOOPOS_ORDER_TITLE_RE +# STRING_WOOPOS_ITEMS_IN_CART -> STRING_WOOPOS_ITEMS_IN_CART_RE +# STRING_WOOPOS_ITEMS_IN_CART_MULTIPLE -> STRING_WOOPOS_ITEMS_IN_CART_MULTIPLE_RE +# STRING_WOOPOS_RECEIPT_SENT_TO_CUSTOMER -> STRING_WOOPOS_RECEIPT_SENT_TO_CUSTOMER_RE +# STRING_WOOPOS_MARK_ORDER_AS_PAID_MESSAGE -> STRING_WOOPOS_MARK_ORDER_AS_PAID_MESSAGE_RE +# STRING_WOOPOS_SCAN_TO_PAY_TOTAL -> STRING_WOOPOS_SCAN_TO_PAY_TOTAL_RE +# STRING_WOOPOS_TAP_TO_PAY_PAYMENT_FAILED_WITH_REASON_MESSAGE -> STRING_WOOPOS_TAP_TO_PAY_PAYMENT_FAILED_WITH_REASON_MESSAGE_RE +# STRING_WOOPOS_TOTALS_SUCCESS_PAYMENT_CASH -> STRING_WOOPOS_TOTALS_SUCCESS_PAYMENT_CASH_RE +# STRING_WOOPOS_TOTALS_SUCCESS_PAYMENT_CARD -> STRING_WOOPOS_TOTALS_SUCCESS_PAYMENT_CARD_RE +# STRING_WOOPOS_TOTALS_SUCCESS_PAYMENT_QR -> STRING_WOOPOS_TOTALS_SUCCESS_PAYMENT_QR_RE +# STRING_WOOPOS_TOTALS_SUCCESS_PAYMENT_EXTERNAL -> STRING_WOOPOS_TOTALS_SUCCESS_PAYMENT_EXTERNAL_RE +# STRING_WOOPOS_VARIATIONS_ANY_VARIATION -> STRING_WOOPOS_VARIATIONS_ANY_VARIATION_RE +# STRING_WOOPOS_ORDERS_ITEMS_SELECTED_COUNT -> STRING_WOOPOS_ORDERS_ITEMS_SELECTED_COUNT_RE +# STRING_WOOPOS_ORDERS_VIA_PAYMENT_METHOD -> STRING_WOOPOS_ORDERS_VIA_PAYMENT_METHOD_RE +# STRING_WOOPOS_ORDERS_CONFIRM_REFUND_TITLE -> STRING_WOOPOS_ORDERS_CONFIRM_REFUND_TITLE_RE +# STRING_WOOPOS_ORDERS_CONFIRM_REFUND_MESSAGE -> STRING_WOOPOS_ORDERS_CONFIRM_REFUND_MESSAGE_RE +# STRING_WOOPOS_ORDERS_REFUND_SUCCESS_MESSAGE -> STRING_WOOPOS_ORDERS_REFUND_SUCCESS_MESSAGE_RE +# STRING_WOOPOS_ORDERS_DETAILS_QTY_UNIT_PRICE_FORMAT -> STRING_WOOPOS_ORDERS_DETAILS_QTY_UNIT_PRICE_FORMAT_RE +# STRING_WOOPOS_ORDERS_DETAILS_BREAKDOWN_DISCOUNT_WITH_CODE_LABEL -> STRING_WOOPOS_ORDERS_DETAILS_BREAKDOWN_DISCOUNT_WITH_CODE_LABEL_RE +# STRING_WOOPOS_ORDERS_DETAILS_REFUND_LABEL_NUMBERED -> STRING_WOOPOS_ORDERS_DETAILS_REFUND_LABEL_NUMBERED_RE +# STRING_WOOPOS_ORDERS_DETAILS_REFUND_ITEMS_SUBTOTAL_ONE -> STRING_WOOPOS_ORDERS_DETAILS_REFUND_ITEMS_SUBTOTAL_ONE_RE +# STRING_WOOPOS_ORDERS_DETAILS_REFUND_ITEMS_SUBTOTAL_OTHER -> STRING_WOOPOS_ORDERS_DETAILS_REFUND_ITEMS_SUBTOTAL_OTHER_RE +# STRING_WOOPOS_CASH_PAYMENT_TOTAL -> STRING_WOOPOS_CASH_PAYMENT_TOTAL_RE +# STRING_WOOPOS_CASH_PAYMENT_CHANGE_DUE -> STRING_WOOPOS_CASH_PAYMENT_CHANGE_DUE_RE +# STRING_WOOPOS_ELIGIBILITY_REASON_UNSUPPORTED_WOOCOMMERCE_VERSION -> STRING_WOOPOS_ELIGIBILITY_REASON_UNSUPPORTED_WOOCOMMERCE_VERSION_RE +# STRING_WOOPOS_SCANNING_SETUP_PAIR_YOUR_SCANNER_MESSAGE -> STRING_WOOPOS_SCANNING_SETUP_PAIR_YOUR_SCANNER_MESSAGE_RE +# STRING_WOOPOS_CARD_READER_FOUND_TITLE -> STRING_WOOPOS_CARD_READER_FOUND_TITLE_RE +# STRING_WOOPOS_CARD_READER_UPDATE_PROGRESS -> STRING_WOOPOS_CARD_READER_UPDATE_PROGRESS_RE +# STRING_WOOPOS_CARD_READER_UPDATE_BATTERY_LOW_MESSAGE_WITH_LEVEL -> STRING_WOOPOS_CARD_READER_UPDATE_BATTERY_LOW_MESSAGE_WITH_LEVEL_RE +# STRING_WOO_SHIPPING_LABELS_CUSTOMS_ITN_REQUIRED_HS_TARIFF_VALUE -> STRING_WOO_SHIPPING_LABELS_CUSTOMS_ITN_REQUIRED_HS_TARIFF_VALUE_RE +# STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_DESCRIPTION_TOO_LONG -> STRING_WOO_SHIPPING_LABELS_CUSTOMS_PRODUCT_DETAILS_DESCRIPTION_TOO_LONG_RE +# STRING_WOO_SHIPPING_SPLIT_SHIPMENT_MOVED_NOTICE_ONE -> STRING_WOO_SHIPPING_SPLIT_SHIPMENT_MOVED_NOTICE_ONE_RE +# STRING_WOO_SHIPPING_SPLIT_SHIPMENT_MOVED_NOTICE_PLURAL -> STRING_WOO_SHIPPING_SPLIT_SHIPMENT_MOVED_NOTICE_PLURAL_RE +# STRING_WOO_SHIPPING_SPLIT_SHIPMENT_SHIPMENT_NAME -> STRING_WOO_SHIPPING_SPLIT_SHIPMENT_SHIPMENT_NAME_RE +# STRING_WOO_SHIPPING_SPLIT_SHIPMENT_SHIPMENT_REMOVE -> STRING_WOO_SHIPPING_SPLIT_SHIPMENT_SHIPMENT_REMOVE_RE +# STRING_WOO_SHIPPING_SPLIT_SHIPMENT_BOTTOM_SHEET_DESC -> STRING_WOO_SHIPPING_SPLIT_SHIPMENT_BOTTOM_SHEET_DESC_RE +# STRING_WOO_SHIPPING_SPLIT_SHIPMENT_BOTTOM_SHEET_DESC_MULTIPLE_SHIPMENT_ONE -> STRING_WOO_SHIPPING_SPLIT_SHIPMENT_BOTTOM_SHEET_DESC_MULTIPLE_SHIPMENT_ONE_RE +# STRING_WOO_SHIPPING_SPLIT_SHIPMENT_BOTTOM_SHEET_DESC_MULTIPLE_SHIPMENT_PLURAL -> STRING_WOO_SHIPPING_SPLIT_SHIPMENT_BOTTOM_SHEET_DESC_MULTIPLE_SHIPMENT_PLURAL_RE +# STRING_WOO_SHIPPING_REFUND_DURATION_DESCRIPTION -> STRING_WOO_SHIPPING_REFUND_DURATION_DESCRIPTION_RE +# STRING_WOO_SHIPPING_REFUND_PURCHASE_DATE -> STRING_WOO_SHIPPING_REFUND_PURCHASE_DATE_RE +# STRING_WOO_SHIPPING_REFUND_AMOUNT_ELIGIBLE_FOR_REFUND -> STRING_WOO_SHIPPING_REFUND_AMOUNT_ELIGIBLE_FOR_REFUND_RE +# STRING_WOO_SHIPPING_PAYMENT_EDIT_DISABLED_WARNING -> STRING_WOO_SHIPPING_PAYMENT_EDIT_DISABLED_WARNING_RE +# STRING_WOO_SHIPPING_PAYMENT_METHODS_INFO_FOOTER -> STRING_WOO_SHIPPING_PAYMENT_METHODS_INFO_FOOTER_RE +# STRING_WOO_SHIPPING_RATE_EXTRA_COST_FORMAT -> STRING_WOO_SHIPPING_RATE_EXTRA_COST_FORMAT_RE +# STRING_WOO_SHIPPING_RATE_SURCHARGE_DESCRIPTION_TEMPLATE -> STRING_WOO_SHIPPING_RATE_SURCHARGE_DESCRIPTION_TEMPLATE_RE +# STRING_ABOUT_AUTOMATTIC_MAIN_PAGE_TITLE -> STRING_ABOUT_AUTOMATTIC_MAIN_PAGE_TITLE_RE +# STRING_ABOUT_AUTOMATTIC_VERSION_LABEL -> STRING_ABOUT_AUTOMATTIC_VERSION_LABEL_RE +# STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_TITLE -> STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_TITLE_RE +# STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_SUMMARY -> STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_SUMMARY_RE +# STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_TITLE_SINGLE -> STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_TITLE_SINGLE_RE +# STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_TITLE_MULTIPLE -> STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_TITLE_MULTIPLE_RE +# STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_SUMMARY_SINGLE -> STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_SUMMARY_SINGLE_RE +# STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_SUMMARY_MULTIPLE -> STRING_AI_ASSISTANT_CONFIRMATION_ORDERS_BULK_UPDATE_SUMMARY_MULTIPLE_RE +# STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_UPDATE_TITLE -> STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_UPDATE_TITLE_RE +# STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_UPDATE_TITLE_WITH_NAME -> STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_UPDATE_TITLE_WITH_NAME_RE +# STRING_AI_ASSISTANT_CONFIRMATION_PRODUCTS_BULK_UPDATE_TITLE_SINGLE -> STRING_AI_ASSISTANT_CONFIRMATION_PRODUCTS_BULK_UPDATE_TITLE_SINGLE_RE +# STRING_AI_ASSISTANT_CONFIRMATION_PRODUCTS_BULK_UPDATE_TITLE_MULTIPLE -> STRING_AI_ASSISTANT_CONFIRMATION_PRODUCTS_BULK_UPDATE_TITLE_MULTIPLE_RE +# STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_VARIATION_UPDATE_TITLE -> STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_VARIATION_UPDATE_TITLE_RE +# STRING_AI_ASSISTANT_CONFIRMATION_GENERIC_TOOL_CALL -> STRING_AI_ASSISTANT_CONFIRMATION_GENERIC_TOOL_CALL_RE +# STRING_AI_ASSISTANT_CONFIRMATION_BULK_ENTRIES_OVERFLOW -> STRING_AI_ASSISTANT_CONFIRMATION_BULK_ENTRIES_OVERFLOW_RE +# STRING_AI_ASSISTANT_CHAT_MESSAGE_USER_CONTENT_DESCRIPTION -> STRING_AI_ASSISTANT_CHAT_MESSAGE_USER_CONTENT_DESCRIPTION_RE +# STRING_AI_ASSISTANT_CHAT_MESSAGE_ASSISTANT_CONTENT_DESCRIPTION -> STRING_AI_ASSISTANT_CHAT_MESSAGE_ASSISTANT_CONTENT_DESCRIPTION_RE +# STRING_AI_ASSISTANT_CHAT_TOOL_ACTIVITY_CONTENT_DESCRIPTION -> STRING_AI_ASSISTANT_CHAT_TOOL_ACTIVITY_CONTENT_DESCRIPTION_RE +# STRING_AI_ASSISTANT_CHAT_EMPTY_STATE_SUGGESTION_CONTENT_DESCRIPTION -> STRING_AI_ASSISTANT_CHAT_EMPTY_STATE_SUGGESTION_CONTENT_DESCRIPTION_RE +# STRING_AI_ASSISTANT_STATS_CARD_OPEN_CONTENT_DESCRIPTION -> STRING_AI_ASSISTANT_STATS_CARD_OPEN_CONTENT_DESCRIPTION_RE +# STRING_AI_ASSISTANT_CHAT_CONFIRM_TOOL -> STRING_AI_ASSISTANT_CHAT_CONFIRM_TOOL_RE +# STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_TITLE_WITH_NAME -> STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_TITLE_WITH_NAME_RE +# STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_SUMMARY_WITH_NAME -> STRING_AI_ASSISTANT_CONFIRMATION_ORDER_UPDATE_SUMMARY_WITH_NAME_RE +# STRING_AI_ASSISTANT_CONFIRMATION_ORDER_REGISTERED_CUSTOMER_DISPLAY_NAME -> STRING_AI_ASSISTANT_CONFIRMATION_ORDER_REGISTERED_CUSTOMER_DISPLAY_NAME_RE +# STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_VARIATION_UPDATE_TITLE_WITH_NAME -> STRING_AI_ASSISTANT_CONFIRMATION_PRODUCT_VARIATION_UPDATE_TITLE_WITH_NAME_RE diff --git a/.maestro/subflows/dashboard_assert_widget_restored.yaml b/.maestro/subflows/dashboard_assert_widget_restored.yaml new file mode 100644 index 000000000000..60495c79451c --- /dev/null +++ b/.maestro/subflows/dashboard_assert_widget_restored.yaml @@ -0,0 +1,5 @@ +appId: com.woocommerce.android.dev +--- +- assertVisible: + id: ".*__${CARD_SLUG}_${output.dashboardOriginalStates[CARD_SLUG]}__.*" + label: "Restored ${CARD_SLUG} dashboard card state persists" diff --git a/.maestro/subflows/dashboard_assert_widget_selected.yaml b/.maestro/subflows/dashboard_assert_widget_selected.yaml new file mode 100644 index 000000000000..33804a9af5ef --- /dev/null +++ b/.maestro/subflows/dashboard_assert_widget_selected.yaml @@ -0,0 +1,25 @@ +appId: com.woocommerce.android.dev +--- +- runFlow: + when: + true: ${output.dashboardOriginalStates[CARD_SLUG] == 'selected' || output.dashboardOriginalStates[CARD_SLUG] == 'unselected'} + commands: + - assertVisible: + id: ".*__${CARD_SLUG}_selected__.*" + label: "Persisted ${CARD_SLUG} dashboard card selection" + +- runFlow: + when: + true: ${output.dashboardOriginalStates[CARD_SLUG] == 'unavailable'} + commands: + - assertVisible: + id: ".*__${CARD_SLUG}_unavailable__.*" + label: "Persisted unavailable ${CARD_SLUG} dashboard card state" + +- runFlow: + when: + true: ${output.dashboardOriginalStates[CARD_SLUG] == 'hidden'} + commands: + - assertVisible: + id: ".*__${CARD_SLUG}_hidden__.*" + label: "Persisted hidden ${CARD_SLUG} dashboard card state" diff --git a/.maestro/subflows/dashboard_exercise_selected_widget.yaml b/.maestro/subflows/dashboard_exercise_selected_widget.yaml new file mode 100644 index 000000000000..0ade3b7efa2f --- /dev/null +++ b/.maestro/subflows/dashboard_exercise_selected_widget.yaml @@ -0,0 +1,28 @@ +appId: com.woocommerce.android.dev +--- +- runFlow: + when: + true: ${output.dashboardOriginalStates[CARD_SLUG] == 'selected'} + commands: + - scrollUntilVisible: + element: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_selected" + direction: DOWN + timeout: 10000 + label: "Find selected ${CARD_SLUG} dashboard card" + - tapOn: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_selected" + label: "Deselect ${CARD_SLUG} dashboard card" + - extendedWaitUntil: + visible: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_unselected" + timeout: 5000 + label: "Verify ${CARD_SLUG} dashboard card deselected" + - tapOn: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_unselected" + label: "Reselect ${CARD_SLUG} dashboard card" + - extendedWaitUntil: + visible: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_selected" + timeout: 5000 + label: "Verify ${CARD_SLUG} dashboard card reselected" diff --git a/.maestro/subflows/dashboard_record_widget_order.yaml b/.maestro/subflows/dashboard_record_widget_order.yaml new file mode 100644 index 000000000000..0e37f0a49b3f --- /dev/null +++ b/.maestro/subflows/dashboard_record_widget_order.yaml @@ -0,0 +1,15 @@ +appId: com.woocommerce.android.dev +--- +- runFlow: + when: + visible: + id: "dashboard_widget_editor_row_0_${CARD_SLUG}_.*" + commands: + - evalScript: ${output.dashboardOriginalOrder[0] = CARD_SLUG} + +- runFlow: + when: + visible: + id: "dashboard_widget_editor_row_1_${CARD_SLUG}_.*" + commands: + - evalScript: ${output.dashboardOriginalOrder[1] = CARD_SLUG} diff --git a/.maestro/subflows/dashboard_record_widget_state.yaml b/.maestro/subflows/dashboard_record_widget_state.yaml new file mode 100644 index 000000000000..1c93dba1a075 --- /dev/null +++ b/.maestro/subflows/dashboard_record_widget_state.yaml @@ -0,0 +1,33 @@ +appId: com.woocommerce.android.dev +--- +- runFlow: + when: + visible: + id: ".*__${CARD_SLUG}_selected__.*" + commands: + - evalScript: ${output.dashboardOriginalStates[CARD_SLUG] = 'selected'} + +- runFlow: + when: + visible: + id: ".*__${CARD_SLUG}_unselected__.*" + commands: + - evalScript: ${output.dashboardOriginalStates[CARD_SLUG] = 'unselected'} + +- runFlow: + when: + visible: + id: ".*__${CARD_SLUG}_unavailable__.*" + commands: + - evalScript: ${output.dashboardOriginalStates[CARD_SLUG] = 'unavailable'} + +- runFlow: + when: + visible: + id: ".*__${CARD_SLUG}_hidden__.*" + commands: + - evalScript: ${output.dashboardOriginalStates[CARD_SLUG] = 'hidden'} + +- assertTrue: + condition: ${output.dashboardOriginalStates[CARD_SLUG] != null} + label: "Record ${CARD_SLUG} dashboard card state" diff --git a/.maestro/subflows/dashboard_restore_widget.yaml b/.maestro/subflows/dashboard_restore_widget.yaml new file mode 100644 index 000000000000..b2d822733554 --- /dev/null +++ b/.maestro/subflows/dashboard_restore_widget.yaml @@ -0,0 +1,20 @@ +appId: com.woocommerce.android.dev +--- +- runFlow: + when: + true: ${output.dashboardOriginalStates[CARD_SLUG] == 'unselected'} + commands: + - scrollUntilVisible: + element: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_selected" + direction: DOWN + timeout: 10000 + label: "Find ${CARD_SLUG} dashboard card to restore" + - tapOn: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_selected" + label: "Restore ${CARD_SLUG} dashboard card to unselected" + - extendedWaitUntil: + visible: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_unselected" + timeout: 5000 + label: "Verify ${CARD_SLUG} dashboard card restored" diff --git a/.maestro/subflows/dashboard_select_widget.yaml b/.maestro/subflows/dashboard_select_widget.yaml new file mode 100644 index 000000000000..9ee0e05a6cb2 --- /dev/null +++ b/.maestro/subflows/dashboard_select_widget.yaml @@ -0,0 +1,41 @@ +appId: com.woocommerce.android.dev +--- +- runFlow: + when: + true: ${output.dashboardOriginalStates[CARD_SLUG] == 'unselected'} + commands: + - scrollUntilVisible: + element: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_unselected" + direction: DOWN + timeout: 10000 + label: "Find unselected ${CARD_SLUG} dashboard card" + - tapOn: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_unselected" + label: "Select ${CARD_SLUG} dashboard card" + - extendedWaitUntil: + visible: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_selected" + timeout: 5000 + label: "Verify ${CARD_SLUG} dashboard card selected" + +- runFlow: + when: + true: ${output.dashboardOriginalStates[CARD_SLUG] == 'unavailable'} + commands: + - scrollUntilVisible: + element: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_unavailable" + direction: DOWN + timeout: 10000 + label: "Account for unavailable ${CARD_SLUG} dashboard card" + - assertVisible: + id: "dashboard_widget_editor_row_.*_${CARD_SLUG}_unavailable" + +- runFlow: + when: + true: ${output.dashboardOriginalStates[CARD_SLUG] == 'hidden'} + commands: + - assertVisible: + id: ".*__${CARD_SLUG}_hidden__.*" + label: "Account for hidden ${CARD_SLUG} dashboard card" diff --git a/.maestro/subflows/ensure_configured_woo_store.yaml b/.maestro/subflows/ensure_configured_woo_store.yaml new file mode 100644 index 000000000000..f9a10abdde16 --- /dev/null +++ b/.maestro/subflows/ensure_configured_woo_store.yaml @@ -0,0 +1,46 @@ +appId: com.woocommerce.android.dev +--- +- runFlow: + file: navigate_to_more_menu.yaml + +- copyTextFrom: + id: "more_menu_store_url" +- evalScript: ${output.configuredCurrentStoreUrl = maestro.copiedText.trim()} + +- runFlow: + when: + true: ${String(output.configuredCurrentStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0] != String(WOO_JETPACK_STORE_URL).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0]} + commands: + - tapOn: + id: "more_menu_store_switcher" + label: "Open store picker for configured fixture" + - extendedWaitUntil: + visible: + id: "sites_recycler" + timeout: 20000 + - scrollUntilVisible: + element: + text: ".*${String(WOO_JETPACK_STORE_URL).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0]}.*" + direction: DOWN + timeout: 20000 + label: "Find configured Woo fixture store" + - tapOn: + text: ".*${String(WOO_JETPACK_STORE_URL).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0]}.*" + label: "Select configured Woo fixture store" + - tapOn: + id: "button_primary" + retryTapIfNoChange: true + label: "Confirm configured Woo fixture store" + - extendedWaitUntil: + visible: + id: "dashboard_container" + timeout: 30000 + +- runFlow: + file: navigate_to_more_menu.yaml +- copyTextFrom: + id: "more_menu_store_url" +- evalScript: ${output.configuredSelectedStoreUrl = maestro.copiedText.trim()} +- assertTrue: + condition: ${String(output.configuredSelectedStoreUrl).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0] == String(WOO_JETPACK_STORE_URL).toLowerCase().split('://').pop().split('/')[0].split('?')[0].split('#')[0].split(':')[0]} + label: "Configured Woo fixture store is selected" diff --git a/.maestro/subflows/ensure_logged_in.yaml b/.maestro/subflows/ensure_logged_in.yaml new file mode 100644 index 000000000000..bde0af67d895 --- /dev/null +++ b/.maestro/subflows/ensure_logged_in.yaml @@ -0,0 +1,95 @@ +# Ensure the app is launched with a logged-in session. +# +# WPCom triggers extra security screens (magic-link, CAPTCHA) once +# the same account logs in too many times in a short window. To stay +# under that threshold, non-login flows MUST reuse the existing +# session instead of re-authenticating on every run. Only login- +# specific flows should `clearState: true` + re-login. +# +# Behavior: +# 1. Launch the app WITHOUT clearing state, so the existing +# session persists across flows. +# 2. Wait for either the dashboard OR a welcome-screen signal to +# appear. +# 3. Happy path — dashboard ("My store") is visible: return. +# 4. Fallback path — the device was never signed in, the session +# was wiped by a previous `clearState`, or WPCom expired the +# token: run the login.yaml subflow. It clearStates and +# authenticates with the selected Jetpack-connected store and +# WP.com credentials (MAESTRO_WOO_JETPACK_STORE_URL, +# MAESTRO_WOO_WPCOM_EMAIL, MAESTRO_WOO_WPCOM_PASSWORD) and +# lands back on the dashboard. +# +# Why the fallback matters: previously this subflow simply asserted +# "My store" was visible and timed out (failing the flow) if the +# user wasn't logged in. That forced the operator to manually run +# login_successful.yaml as a prerequisite every time they wanted to +# kick off a single non-login flow from a cold emulator. Now any +# flow can be invoked in isolation and it'll self-heal into a +# logged-in state on first run. +appId: com.woocommerce.android.dev +--- +- launchApp: + clearState: false + +# Wait for either the dashboard OR a welcome-screen signal to +# appear. +# +# "My store" — bottom-nav / top-bar label shown +# once logged in +# "Enter your store address" — primary welcome-screen CTA +# "Log in" — welcome-screen alternate CTA +# "Skip" — intro carousel skip button (the +# first screen after clearState on +# some app versions) +# +# Matching any of these tells us the app has finished booting. We +# branch on which one fired. +- extendedWaitUntil: + visible: ".*My store.*|.*Enter your store address.*|.*Log in.*|.*Skip.*" + timeout: 45000 + label: "Wait for dashboard or login welcome screen" + +# Dismiss known post-launch modals before deciding whether the session is dirty. +- runFlow: + when: + visible: "Save" + commands: + - tapOn: + text: "Save" + label: "recovery_dismiss_privacy_dialog" + +- runFlow: + when: + visible: ".*Got it.*" + commands: + - tapOn: + text: ".*Got it.*" + label: "recovery_dismiss_got_it_dialog" + +- runFlow: + when: + visible: ".*Not now.*|.*Maybe later.*" + commands: + - tapOn: + text: ".*Not now.*|.*Maybe later.*" + label: "recovery_dismiss_optional_prompt" + +# Fallback: if we're not on the dashboard, run the full login +# subflow. login.yaml clearStates + authenticates with the primary +# Woo account and lands on the dashboard. Running it when we're +# already on the welcome screen just means the clearState at its +# top is a no-op-ish re-launch — still idempotent. +- runFlow: + when: + notVisible: "My store" + commands: + - runFlow: + file: login.yaml + +# Confirm we're on the dashboard, whether we came in already +# authenticated or via the login fallback. +- extendedWaitUntil: + visible: "My store" + timeout: 30000 + label: "Verify dashboard is reachable" diff --git a/.maestro/subflows/login.yaml b/.maestro/subflows/login.yaml new file mode 100644 index 000000000000..0c8e2dc799c8 --- /dev/null +++ b/.maestro/subflows/login.yaml @@ -0,0 +1,115 @@ +# Reusable login subflow +# Logs into the WooCommerce app with store URL + email + password. +# +# Required env vars: WOO_JETPACK_STORE_URL, WOO_WPCOM_EMAIL, WOO_WPCOM_PASSWORD +appId: com.woocommerce.android.dev +--- +- launchApp: + clearState: true + +# Wait for app to finish loading after clearState (splash screen can take a while) +- extendedWaitUntil: + visible: ".*Skip.*|.*Enter your store address.*|.*Log in.*" + timeout: 30000 + +# Skip the carousel if it appears +- runFlow: + when: + visible: "Skip" + commands: + - tapOn: "Skip" + +# Tap "Enter your store address" on the Welcome screen +- extendedWaitUntil: + visible: + id: "button_login_store" + timeout: 15000 +- tapOn: + id: "button_login_store" + retryTapIfNoChange: true + +# Enter store URL +- extendedWaitUntil: + visible: + id: "input" + timeout: 10000 +- tapOn: + id: "input" +- inputText: ${WOO_JETPACK_STORE_URL} +- hideKeyboard +- tapOn: + id: "bottom_button" + retryTapIfNoChange: true + +# Enter email address +- extendedWaitUntil: + visible: + id: "login_continue_button" + timeout: 15000 +- tapOn: + id: "input" +- inputText: ${WOO_WPCOM_EMAIL} +- hideKeyboard +- tapOn: + id: "login_continue_button" + retryTapIfNoChange: true + +# Dismiss Google Password Manager if it appears +- runFlow: + when: + visible: "No thanks" + commands: + - tapOn: "No thanks" + +# Handle Magic Link screen - tap "Enter your password instead" +- runFlow: + when: + visible: + id: "login_enter_password" + commands: + - tapOn: + id: "login_enter_password" + +# Dismiss Google Password Manager again if it appears on password screen +- runFlow: + when: + visible: "No thanks" + commands: + - tapOn: "No thanks" + +# Enter password +- extendedWaitUntil: + visible: + id: "input" + timeout: 10000 +- tapOn: + id: "input" +- inputText: ${WOO_WPCOM_PASSWORD} +- hideKeyboard +- tapOn: + id: "bottom_button" + retryTapIfNoChange: true + +# Wait for dashboard to load (or privacy dialog) +- extendedWaitUntil: + visible: ".*My store.*|.*Manage privacy.*" + timeout: 30000 + +# Dismiss privacy dialog if it appears +- runFlow: + when: + visible: "Save" + commands: + - tapOn: "Save" + +# Dismiss any store setup or onboarding dialogs +- runFlow: + when: + visible: ".*Got it.*" + commands: + - tapOn: ".*Got it.*" + +# Verify dashboard is loaded +- extendedWaitUntil: + visible: "My store" + timeout: 10000 diff --git a/.maestro/subflows/navigate_to_more_menu.yaml b/.maestro/subflows/navigate_to_more_menu.yaml new file mode 100644 index 000000000000..17a36606b63f --- /dev/null +++ b/.maestro/subflows/navigate_to_more_menu.yaml @@ -0,0 +1,9 @@ +# Navigate to the More Menu (Hub) tab +appId: com.woocommerce.android.dev +--- +- tapOn: + id: "moreMenu" +- extendedWaitUntil: + visible: + id: "more_menu_compose_view" + timeout: 10000 diff --git a/.maestro/subflows/navigate_to_orders.yaml b/.maestro/subflows/navigate_to_orders.yaml new file mode 100644 index 000000000000..3317963164ea --- /dev/null +++ b/.maestro/subflows/navigate_to_orders.yaml @@ -0,0 +1,9 @@ +# Navigate to the Orders tab +appId: com.woocommerce.android.dev +--- +- tapOn: + id: "orders" +- extendedWaitUntil: + visible: + id: "ordersList" + timeout: 15000 diff --git a/.maestro/subflows/navigate_to_products.yaml b/.maestro/subflows/navigate_to_products.yaml new file mode 100644 index 000000000000..2ce5cec86796 --- /dev/null +++ b/.maestro/subflows/navigate_to_products.yaml @@ -0,0 +1,9 @@ +# Navigate to the Products tab +appId: com.woocommerce.android.dev +--- +- tapOn: + id: "products" +- extendedWaitUntil: + visible: + id: "productsRecycler" + timeout: 15000 diff --git a/.maestro/subflows/open_dashboard_widget_editor.yaml b/.maestro/subflows/open_dashboard_widget_editor.yaml new file mode 100644 index 000000000000..c78a15cb08a9 --- /dev/null +++ b/.maestro/subflows/open_dashboard_widget_editor.yaml @@ -0,0 +1,20 @@ +appId: com.woocommerce.android.dev +--- +- extendedWaitUntil: + visible: + id: "dashboard_container" + timeout: 20000 + label: "Wait for dashboard before customization" + +- tapOn: + id: "menu_edit_screen_widgets" + retryTapIfNoChange: true + label: "Open dashboard customization" + +- extendedWaitUntil: + visible: + id: "dashboard_widget_editor_state__.*" + timeout: 15000 + label: "Wait for dashboard card state" + +- assertVisible: "SAVE" diff --git a/.maestro/subflows/pos_add_discovered_variable_product.yaml b/.maestro/subflows/pos_add_discovered_variable_product.yaml new file mode 100644 index 000000000000..1a17d09ff0cf --- /dev/null +++ b/.maestro/subflows/pos_add_discovered_variable_product.yaml @@ -0,0 +1,57 @@ +# Discovers a variable product from the loaded POS catalog, searches for its +# app-provided name, opens its variations, and adds the first variation. +appId: com.woocommerce.android.dev +--- +- scrollUntilVisible: + element: "Variable Product .*" + direction: DOWN + timeout: 30000 + label: "Find a Variable product in the POS catalog" + +- copyTextFrom: + text: "Variable Product .*" + +- evalScript: ${output.posVariableProductName = maestro.copiedText.replace('Variable Product ', '')} +- assertTrue: + condition: ${output.posVariableProductName.length > 0} + label: "Capture the discovered Variable product name" + +- tapOn: + id: "woo_pos_search_input" + label: "Open product search" + +- extendedWaitUntil: + visible: ".*Search products and variations.*" + timeout: 10000 + +- tapOn: + id: "woo_pos_search_input" + label: "Focus product search" +- inputText: ${output.posVariableProductName} +- hideKeyboard + +- extendedWaitUntil: + visible: "Variable Product .*" + timeout: 20000 + label: "Search resolves a Variable product" + +- tapOn: + id: "woo_pos_product_item" + index: 0 + label: "Open discovered Variable product variations" + +- extendedWaitUntil: + notVisible: ".*Search products and variations.*" + timeout: 10000 + label: "Require variation selection screen" + +- extendedWaitUntil: + visible: + id: "woo_pos_product_item" + timeout: 20000 + label: "Wait for POS variations" + +- tapOn: + id: "woo_pos_product_item" + index: 0 + label: "Add first variation to cart" diff --git a/.maestro/toolchain.properties b/.maestro/toolchain.properties new file mode 100644 index 000000000000..36b1240fe36f --- /dev/null +++ b/.maestro/toolchain.properties @@ -0,0 +1,3 @@ +maestro=2.8.0 +maestro_sha256=b3e561161904fb391875ca5834d5b22cf0b01c052dd1b408ad83e30d8f8951b3 +java=21 diff --git a/.mcp.json b/.mcp.json index 7f0082a09383..a2dd63180357 100644 --- a/.mcp.json +++ b/.mcp.json @@ -4,6 +4,10 @@ "command": "npx", "args": ["-y", "@mobilenext/mobile-mcp@latest"] }, + "maestro": { + "command": "maestro", + "args": ["mcp"] + }, "context-a8c": { "command": "npx", "args": ["-y", "@automattic/mcp-context-a8c@latest"] diff --git a/Dangerfile b/Dangerfile index fa44a1c02e8f..442faa8656ed 100644 --- a/Dangerfile +++ b/Dangerfile @@ -43,7 +43,7 @@ pr_size_checker.check_diff_size( } ) -android_unit_test_checker.check_missing_tests +android_unit_test_checker.check_missing_tests(path_exceptions: ['.maestro/scripts/tests/*.py']) # skip remaining checks if the PR is still a Draft if github.pr_draft? diff --git a/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartScreenTest.kt b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartScreenTest.kt new file mode 100644 index 000000000000..687d6c73b4e8 --- /dev/null +++ b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartScreenTest.kt @@ -0,0 +1,63 @@ +package com.woocommerce.android.ui.woopos.home.cart + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTheme +import com.woocommerce.android.ui.woopos.util.WooPosTestTags +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class WooPosCartScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun productRowDoesNotExposeCouponTestTag() { + setCartItem( + WooPosCartItemViewState.Product.Simple( + itemNumber = 1, + id = 1, + name = "Product", + price = "$10.00", + description = null, + imageUrl = null, + ) + ) + + composeTestRule.onNodeWithTag(WooPosTestTags.CART_COUPON_ITEM).assertDoesNotExist() + } + + @Test + fun couponRowExposesCouponTestTag() { + setCartItem( + WooPosCartItemViewState.Coupon( + itemNumber = 1, + id = 1, + name = "Coupon", + summary = "10% off", + ) + ) + + composeTestRule.onNodeWithTag(WooPosTestTags.CART_COUPON_ITEM).assertIsDisplayed() + } + + private fun setCartItem(item: WooPosCartItemViewState) { + composeTestRule.setContent { + WooPosTheme { + WooPosCartScreen( + state = WooPosCartState( + body = WooPosCartState.Body.WithItems(listOf(item)), + checkoutButtonState = WooPosCartState.CheckoutButtonState.Invisible, + ), + onUIEvent = {}, + checkoutSlot = WooPosCartCheckoutButtonSlot.External, + ) + } + } + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/DragAndDropItemsList.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/DragAndDropItemsList.kt index a53c17399328..742f16976f96 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/DragAndDropItemsList.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/DragAndDropItemsList.kt @@ -93,6 +93,8 @@ fun DragAndDropSelectableItem( onSelectionChange: (T, Boolean) -> Unit, itemKey: (item: T) -> Any, modifier: Modifier = Modifier, + rowModifier: Modifier = Modifier, + dragHandleModifier: Modifier = Modifier, itemFormatter: @Composable T.() -> String = { toString() }, isEnabled: Boolean = true, ) { @@ -105,7 +107,7 @@ fun DragAndDropSelectableItem( .padding(16.dp) } Row( - modifier = itemModifier, + modifier = rowModifier.then(itemModifier), verticalAlignment = Alignment.CenterVertically ) { SelectionCheck( @@ -123,7 +125,7 @@ fun DragAndDropSelectableItem( Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_drag_handle_24dp), contentDescription = stringResource(id = R.string.drag_handle), - modifier = Modifier + modifier = dragHandleModifier .dragContainerForDragHandle( dragDropState = dragDropState, key = itemKey(item), diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/coupons/edit/EditCouponScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/coupons/edit/EditCouponScreen.kt index 0e93047fc2f3..bac83b9713be 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/coupons/edit/EditCouponScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/coupons/edit/EditCouponScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource @@ -132,7 +133,8 @@ fun EditCouponScreen( text = stringResource(id = viewState.saveButtonText), modifier = Modifier .padding(horizontal = dimensionResource(id = R.dimen.major_100)) - .fillMaxWidth(), + .fillMaxWidth() + .testTag(EditCouponTestTags.SAVE_BUTTON), enabled = viewState.hasChanges ) } @@ -177,7 +179,9 @@ private fun DetailsSection( onValueChange = { onCouponCodeChanged(it.toLowerCase(Locale.current)) }, helperText = stringResource(id = R.string.coupon_edit_code_helper), keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Characters), - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testTag(EditCouponTestTags.CODE_INPUT) ) WCTextButton( onClick = { @@ -331,10 +335,18 @@ private fun AmountField(amount: BigDecimal?, amountUnit: String, type: Type?, on // TODO use KeyboardType.Decimal after updating to Compose 1.2.0 // (https://issuetracker.google.com/issues/209835363) keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testTag(EditCouponTestTags.AMOUNT_INPUT) ) } +internal object EditCouponTestTags { + const val AMOUNT_INPUT = "edit_coupon_amount_input" + const val CODE_INPUT = "edit_coupon_code_input" + const val SAVE_BUTTON = "edit_coupon_save_button" +} + @Composable private fun DescriptionButton(description: String?, onButtonClicked: () -> Unit) { WCOutlinedButton( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customer/CustomerListScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customer/CustomerListScreen.kt index 8a0b22bf3141..820f5d677e67 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customer/CustomerListScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customer/CustomerListScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource @@ -195,10 +196,15 @@ private fun CustomerListItem( name = customer.name.render(), username = customer.username.render(), email = customer.email.render(), + modifier = Modifier.testTag(CustomerListTestTags.CUSTOMER_ITEM), onClick = { onCustomerSelected(customer.payload) }, ) } +internal object CustomerListTestTags { + const val CUSTOMER_ITEM = "customer_list_item" +} + @Composable private fun CustomerListViewState.CustomerList.Item.Customer.Text.render() = when (this) { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/widgeteditor/DashboardWidgetEditorScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/widgeteditor/DashboardWidgetEditorScreen.kt index 5f9eb30ef79d..ee2f8b919112 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/widgeteditor/DashboardWidgetEditorScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/dashboard/widgeteditor/DashboardWidgetEditorScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -37,30 +38,33 @@ import com.woocommerce.android.ui.compose.component.DragAndDropSelectableItem fun DashboardWidgetEditorScreen(viewModel: DashboardWidgetEditorViewModel) { BackHandler(onBack = viewModel::onBackPressed) viewModel.viewState.observeAsState().value?.let { state -> - Scaffold(topBar = { - TopAppBar( - title = { Text(text = stringResource(id = R.string.my_store_edit_screen_widgets)) }, - navigationIcon = { - IconButton(viewModel::onBackPressed) { - Icon( - ImageVector.vectorResource(R.drawable.ic_close_24dp), - contentDescription = stringResource(id = R.string.back) - ) - } - }, - backgroundColor = colorResource(id = R.color.color_toolbar), - actions = { - TextButton( - onClick = viewModel::onSaveClicked, - enabled = state.isSaveButtonEnabled - ) { - Text( - text = stringResource(id = R.string.save).uppercase() - ) - } - }, - ) - }) { padding -> + Scaffold( + modifier = Modifier.testTag(state.widgetStateTestTag), + topBar = { + TopAppBar( + title = { Text(text = stringResource(id = R.string.my_store_edit_screen_widgets)) }, + navigationIcon = { + IconButton(viewModel::onBackPressed) { + Icon( + ImageVector.vectorResource(R.drawable.ic_close_24dp), + contentDescription = stringResource(id = R.string.back) + ) + } + }, + backgroundColor = colorResource(id = R.color.color_toolbar), + actions = { + TextButton( + onClick = viewModel::onSaveClicked, + enabled = state.isSaveButtonEnabled + ) { + Text( + text = stringResource(id = R.string.save).uppercase() + ) + } + }, + ) + } + ) { padding -> when { state.isLoading -> LoadWidgetsConfiguration() else -> { @@ -73,6 +77,7 @@ fun DashboardWidgetEditorScreen(viewModel: DashboardWidgetEditorViewModel) { .padding(padding), isItemDraggable = { it.isAvailable } ) { item, dragDropState -> + val itemIndex = state.orderedWidgetList.indexOf(item) when (item.isAvailable) { true -> { val selectedItems = state.orderedWidgetList.filter { it.isVisible } @@ -83,12 +88,17 @@ fun DashboardWidgetEditorScreen(viewModel: DashboardWidgetEditorViewModel) { onSelectionChange = viewModel::onSelectionChange, itemKey = { it.type }, itemFormatter = { stringResource(id = item.title) }, - isEnabled = !item.isSelected || selectedItems.size > 1 + isEnabled = !item.isSelected || selectedItems.size > 1, + rowModifier = Modifier.testTag(item.rowTestTag(itemIndex)), + dragHandleModifier = Modifier.testTag(item.dragHandleTestTag(itemIndex)) ) } false -> { - UnavailableWidget(item) + UnavailableWidget( + widget = item, + modifier = Modifier.testTag(item.rowTestTag(itemIndex)) + ) } } } @@ -105,6 +115,27 @@ fun DashboardWidgetEditorScreen(viewModel: DashboardWidgetEditorViewModel) { } } +private val DashboardWidgetEditorViewModel.WidgetEditorState.widgetStateTestTag: String + get() = widgetList.joinToString( + prefix = "dashboard_widget_editor_state__", + separator = "__", + postfix = "__" + ) { widget -> "${widget.type.trackingIdentifier}_${widget.editorStateTag}" } + +private fun DashboardWidget.rowTestTag(index: Int) = + "dashboard_widget_editor_row_${index}_${type.trackingIdentifier}_$editorStateTag" + +private fun DashboardWidget.dragHandleTestTag(index: Int) = + "dashboard_widget_editor_drag_handle_${index}_${type.trackingIdentifier}" + +private val DashboardWidget.editorStateTag: String + get() = when { + status is DashboardWidget.Status.Hidden -> "hidden" + status is DashboardWidget.Status.Unavailable -> "unavailable" + isSelected -> "selected" + else -> "unselected" + } + @Composable private fun UnavailableWidget( widget: DashboardWidget, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/moremenu/MoreMenuScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/moremenu/MoreMenuScreen.kt index f1b30496aa59..fd7105e58bfb 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/moremenu/MoreMenuScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/moremenu/MoreMenuScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource @@ -177,7 +178,9 @@ private fun HeaderButton( contentPadding = PaddingValues(dimensionResource(id = R.dimen.major_75)), colors = headerBackgroundColors, shape = RoundedCornerShape(dimensionResource(id = R.dimen.major_75)), - modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.major_100)), + modifier = Modifier + .testTag("more_menu_store_switcher") + .padding(horizontal = dimensionResource(id = R.dimen.major_100)), content = content ) } @@ -207,6 +210,7 @@ private fun HeaderContent( maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier + .testTag("more_menu_store_title") .align(Alignment.CenterVertically) .weight(1f, false) ) @@ -217,7 +221,9 @@ private fun HeaderContent( Text( text = siteUrl, style = MaterialTheme.typography.caption, - modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.minor_50)) + modifier = Modifier + .testTag("more_menu_store_url") + .padding(vertical = dimensionResource(id = R.dimen.minor_50)) ) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/shipping/AmountBigDecimalTextField.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/shipping/AmountBigDecimalTextField.kt index bd0fa4a4e516..f17051b86586 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/shipping/AmountBigDecimalTextField.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/shipping/AmountBigDecimalTextField.kt @@ -33,6 +33,7 @@ fun AmountBigDecimalTextField( boxBackgroundMode = TextInputLayout.BOX_BACKGROUND_NONE val textSize = 28f editText.apply { + id = R.id.order_shipping_amount_input background = null setTextAppearance(R.style.TextAppearance_Woo_EditText) setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/shipping/OrderShippingScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/shipping/OrderShippingScreen.kt index 31e31e6d5980..99f69601ffb1 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/shipping/OrderShippingScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/shipping/OrderShippingScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -138,7 +139,9 @@ fun UpdateShippingScreen( FieldEditValue( name.orEmpty(), { name -> onNameChanged(name) }, - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() + .testTag(OrderShippingTestTags.NAME_INPUT) ) } Column( @@ -163,7 +166,9 @@ fun UpdateShippingScreen( WCColoredButton( enabled = isSaveChangesEnabled, onClick = { onSaveChanges() }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testTag(OrderShippingTestTags.SAVE_BUTTON) ) { val buttonResId = if (isEditFlow) { R.string.order_creation_shipping_edit @@ -176,6 +181,11 @@ fun UpdateShippingScreen( } } +internal object OrderShippingTestTags { + const val NAME_INPUT = "order_shipping_name_input" + const val SAVE_BUTTON = "order_shipping_save_button" +} + @Composable fun FieldCaption( text: String, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/views/ExpandableProductCard.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/views/ExpandableProductCard.kt index bc73ba11858d..97133cb56d9a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/views/ExpandableProductCard.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/creation/views/ExpandableProductCard.kt @@ -53,6 +53,7 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource @@ -83,6 +84,12 @@ import com.woocommerce.android.util.getVariationAttributesAndStockText import java.math.BigDecimal const val ANIM_DURATION_MILLIS = 128 + +internal object ExpandableProductCardTestTags { + const val PRODUCT_CARD = "order_product_card" + const val DISCOUNT_AMOUNT = "order_product_discount_amount" +} + const val MULTIPLICATION_CHAR = "×" @SuppressLint("UnusedTransitionTargetStateParameter") @@ -110,6 +117,7 @@ fun ExpandableProductCard( } ConstraintLayout( modifier = Modifier + .testTag(ExpandableProductCardTestTags.PRODUCT_CARD) .fillMaxWidth() .clickable( interactionSource = remember { MutableInteractionSource() }, @@ -168,6 +176,7 @@ fun ExpandableProductCard( if (!isExpanded && product.productInfo.hasDiscount) { Text( modifier = Modifier + .testTag(ExpandableProductCardTestTags.DISCOUNT_AMOUNT) .constrainAs(discount) { end.linkTo(chevron.start) top.linkTo(stock.top) @@ -414,6 +423,7 @@ fun ExtendedProductCardContent( } Text( modifier = Modifier + .testTag(ExpandableProductCardTestTags.DISCOUNT_AMOUNT) .padding(horizontal = dimensionResource(id = R.dimen.minor_100)) .constrainAs(discountAmount) { end.linkTo(parent.end) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/selector/ProductSelectorScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/selector/ProductSelectorScreen.kt index 220e04ac5677..d73dbe0425b5 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/selector/ProductSelectorScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/selector/ProductSelectorScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource @@ -68,6 +69,7 @@ import com.woocommerce.android.ui.products.selector.ProductSelectorViewModel.Sel import com.woocommerce.android.ui.products.selector.ProductSelectorViewModel.ViewState import com.woocommerce.android.ui.products.selector.SelectionState.PARTIALLY_SELECTED import com.woocommerce.android.ui.products.selector.SelectionState.SELECTED +import com.woocommerce.android.ui.products.selector.components.ProductSelectorTestTags import com.woocommerce.android.ui.products.selector.components.SelectorListItem import com.woocommerce.android.util.StringUtils @@ -324,6 +326,7 @@ private fun displayProductsSection( imageContentDescription = stringResource(string.product_image_content_description), isCogwheelVisible = product is ListItem.ConfigurableListItem, enabled = state.selectionEnabled && product.enabled, + testTag = product.selectorTestTag, onEditConfiguration = { (product as? ListItem.ConfigurableListItem)?.let(onEditConfiguration) } @@ -346,6 +349,13 @@ enum class ProductType { RECENT } +private val ListItem.selectorTestTag: String + get() = if (hasVariations()) { + ProductSelectorTestTags.VARIABLE_PRODUCT_ITEM + } else { + ProductSelectorTestTags.PRODUCT_ITEM + } + @Composable private fun ProductList( state: ViewState, @@ -446,6 +456,7 @@ private fun ProductList( imageContentDescription = stringResource(string.product_image_content_description), isCogwheelVisible = product is ListItem.ConfigurableListItem, enabled = state.selectionEnabled && product.enabled, + testTag = product.selectorTestTag, onEditConfiguration = { (product as? ListItem.ConfigurableListItem)?.let(onEditConfiguration) } @@ -505,6 +516,7 @@ private fun SelectionConfirmButton( }, enabled = state.isDoneButtonEnabled, modifier = Modifier + .testTag(ProductSelectorTestTags.DONE_BUTTON) .fillMaxWidth() .padding(dimensionResource(id = dimen.major_100)) ) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/selector/components/SelectorListItem.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/selector/components/SelectorListItem.kt index e2a883e63726..2e3fa19e163d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/selector/components/SelectorListItem.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/selector/components/SelectorListItem.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource @@ -54,10 +55,12 @@ fun SelectorListItem( isCogwheelVisible: Boolean, enabled: Boolean, onEditConfiguration: () -> Unit, + testTag: String? = null, onItemClick: () -> Unit, ) { Row( modifier = Modifier + .optionalTestTag(testTag) .clickable( enabled = enabled, role = Role.Button, @@ -167,6 +170,16 @@ fun SelectorListItem( } } +object ProductSelectorTestTags { + const val PRODUCT_ITEM = "product_selector_product_item" + const val VARIABLE_PRODUCT_ITEM = "product_selector_variable_product_item" + const val VARIATION_ITEM = "product_selector_variation_item" + const val DONE_BUTTON = "product_selector_done_button" +} + +private fun Modifier.optionalTestTag(tag: String?): Modifier = + if (tag == null) this else testTag(tag) + @Composable private fun SelectorListItemInfo( summary: String, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/variations/selector/VariationSelectorScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/variations/selector/VariationSelectorScreen.kt index 27c27223e8e2..390c894350e5 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/variations/selector/VariationSelectorScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/variations/selector/VariationSelectorScreen.kt @@ -51,6 +51,7 @@ import com.woocommerce.android.ui.compose.component.InfiniteListHandler import com.woocommerce.android.ui.compose.component.TopAppBarEdgeToEdge import com.woocommerce.android.ui.compose.component.WCTextButton import com.woocommerce.android.ui.products.selector.SelectionState.SELECTED +import com.woocommerce.android.ui.products.selector.components.ProductSelectorTestTags import com.woocommerce.android.ui.products.selector.components.SelectorListItem import com.woocommerce.android.ui.products.variations.selector.VariationSelectorViewModel.LoadingState.APPENDING import com.woocommerce.android.ui.products.variations.selector.VariationSelectorViewModel.LoadingState.LOADING @@ -186,6 +187,7 @@ private fun VariationList( imageContentDescription = stringResource(string.product_image_content_description), isCogwheelVisible = false, enabled = true, + testTag = ProductSelectorTestTags.VARIATION_ITEM, onEditConfiguration = {} ) { onVariationClick(variation) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosSearchInput.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosSearchInput.kt index 842e20fa6493..edc61dd7bc33 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosSearchInput.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosSearchInput.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.TextRange @@ -54,6 +55,7 @@ import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosThe import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTypography import com.woocommerce.android.ui.woopos.home.items.WOO_POS_ITEMS_TOOLBAR_HEIGHT import com.woocommerce.android.ui.woopos.home.items.WooPosItemsUIEvent +import com.woocommerce.android.ui.woopos.util.WooPosTestTags import kotlinx.coroutines.delay import kotlinx.parcelize.Parcelize @@ -70,7 +72,7 @@ fun WooPosSearchInput( ) Box( - modifier = modifier, + modifier = modifier.testTag(WooPosTestTags.SEARCH_INPUT), contentAlignment = Alignment.CenterEnd ) { when (state) { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/designsystem/WooPosTheme.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/designsystem/WooPosTheme.kt index dee10ba9f2eb..3341d5d63e6a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/designsystem/WooPosTheme.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/designsystem/WooPosTheme.kt @@ -3,6 +3,7 @@ package com.woocommerce.android.ui.woopos.common.composeui.designsystem import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.darkColorScheme @@ -10,7 +11,10 @@ import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId data class CustomColors( val success: Color, @@ -218,7 +222,9 @@ fun WooPosTheme(content: @Composable () -> Unit) { @Composable private fun SurfacedContent(content: @Composable () -> Unit) { Surface(color = MaterialTheme.colorScheme.surface) { - content() + Box(Modifier.semantics { testTagsAsResourceId = true }) { + content() + } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartScreen.kt index b0725d1a16d3..f7457a6ee371 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartScreen.kt @@ -485,6 +485,7 @@ private fun ClearCartButton( Box(modifier = modifier) { WooPosIconButton( + modifier = Modifier.testTag(WooPosTestTags.CLEAR_CART_BUTTON), icon = ImageVector.vectorResource(R.drawable.ic_delete_24dp), enabled = !dropdownExpanded, onClick = { dropdownExpanded = true }, @@ -638,6 +639,7 @@ private fun CouponItem( WooPosCard( modifier = modifier .wrapContentHeight() + .testTag(WooPosTestTags.CART_COUPON_ITEM) .semantics { contentDescription = itemContentDescription }, backgroundColor = MaterialTheme.colorScheme.surfaceContainerLowest, elevation = WooPosElevation.Medium, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsList.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsList.kt index bfead25a8c69..9004bc7d4f4e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsList.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsList.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -367,7 +368,10 @@ fun WooPosCouponCard( enabled = item.expiredState is Coupon.ExpiredState.NotExpired, onClickLabel = stringResource(R.string.woopos_add_coupon_to_cart_accessibility_label) ) { onItemClicked(item) } - .clearAndSetSemantics { contentDescription = itemContentDescription } + .clearAndSetSemantics { + contentDescription = itemContentDescription + testTag = WooPosTestTags.COUPON_ADD_TO_CART_BUTTON + } .height(IntrinsicSize.Min) .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically diff --git a/WooCommerce/src/main/res/values/ids.xml b/WooCommerce/src/main/res/values/ids.xml index 6e29705c2cde..9cfc21c91545 100644 --- a/WooCommerce/src/main/res/values/ids.xml +++ b/WooCommerce/src/main/res/values/ids.xml @@ -5,4 +5,5 @@ + diff --git a/libs/pos/src/main/java/com/woocommerce/android/ui/woopos/util/WooPosTestTags.kt b/libs/pos/src/main/java/com/woocommerce/android/ui/woopos/util/WooPosTestTags.kt index cc449cf456ae..72559cee70e4 100644 --- a/libs/pos/src/main/java/com/woocommerce/android/ui/woopos/util/WooPosTestTags.kt +++ b/libs/pos/src/main/java/com/woocommerce/android/ui/woopos/util/WooPosTestTags.kt @@ -2,6 +2,8 @@ package com.woocommerce.android.ui.woopos.util object WooPosTestTags { const val PRODUCT_ITEM = "woo_pos_product_item" + const val SEARCH_INPUT = "woo_pos_search_input" + const val COUPON_ADD_TO_CART_BUTTON = "woo_pos_coupon_add_to_cart_button" const val CHECKOUT_BUTTON = "woo_pos_checkout_button" const val CASH_PAYMENT_BUTTON = "woo_pos_cash_payment_button" const val CARD_READER_PAYMENT_BUTTON = "woo_pos_card_reader_payment_button" @@ -19,4 +21,6 @@ object WooPosTestTags { const val NEW_ORDER_BUTTON = "woo_pos_new_order_button" const val SUCCESS_CHECKMARK_ICON = "woo_pos_success_checkmark_icon" const val CART_ITEMS_COUNT = "woo_pos_cart_items_count" + const val CLEAR_CART_BUTTON = "woo_pos_clear_cart_button" + const val CART_COUPON_ITEM = "woo_pos_cart_coupon_item" }