From 68b0055cfecfd716df34036affd04e517971a236 Mon Sep 17 00:00:00 2001 From: Suriya Date: Tue, 17 Mar 2026 15:21:06 +0530 Subject: [PATCH] comprehensive cases --- .../comprehensive_billing_flow.yaml | 62 ++++ .../hub_management_lifecycle_e2e.yaml | 54 ++++ .../orders/debug/staff_search_only.yaml | 85 ++++++ .../comprehensive_order_lifecycle.yaml | 118 ++++++++ .../happy_path/create_order_one_time_e2e.yaml | 44 ++- .../cross_app_order_verification.yaml | 180 ++++++++++++ .../happy_path/full_lifecycle_multiapp.yaml | 272 ++++++++++++++++++ .../reorder_historical_data_e2e.yaml | 53 ++++ .../comprehensive_reports_verification.yaml | 62 ++++ .../maestro/scripts/generate_order_name.js | 1 + .../plugins/GeneratedPluginRegistrant.java | 5 + .../ios/Runner/GeneratedPluginRegistrant.m | 7 + .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + .../persistent_profile_edit_e2e.yaml | 44 +++ .../shifts/confirm_booking_dialog.yaml | 4 + .../happy_path/geofence_clock_in_e2e.yaml | 59 ++++ .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + .../shifts_connector_repository_impl.dart | 10 +- .../widgets/shift_order_form_sheet.dart | 1 + .../one_time_order_event_name_input.dart | 1 + .../otp_verification/otp_input_field.dart | 182 +++++------- .../staff/authentication/pubspec.yaml | 2 + .../src/presentation/pages/shifts_page.dart | 150 +++++----- .../presentation/widgets/my_shift_card.dart | 7 +- .../widgets/tabs/find_shifts_tab.dart | 34 ++- .../widgets/staff_main_bottom_bar.dart | 40 +-- apps/mobile/pubspec.lock | 24 ++ 30 files changed, 1285 insertions(+), 227 deletions(-) create mode 100644 apps/mobile/apps/client/maestro/billing/happy_path/comprehensive_billing_flow.yaml create mode 100644 apps/mobile/apps/client/maestro/hubs/happy_path/hub_management_lifecycle_e2e.yaml create mode 100644 apps/mobile/apps/client/maestro/orders/debug/staff_search_only.yaml create mode 100644 apps/mobile/apps/client/maestro/orders/happy_path/comprehensive_order_lifecycle.yaml create mode 100644 apps/mobile/apps/client/maestro/orders/happy_path/cross_app_order_verification.yaml create mode 100644 apps/mobile/apps/client/maestro/orders/happy_path/full_lifecycle_multiapp.yaml create mode 100644 apps/mobile/apps/client/maestro/orders/happy_path/reorder_historical_data_e2e.yaml create mode 100644 apps/mobile/apps/client/maestro/reports/happy_path/comprehensive_reports_verification.yaml create mode 100644 apps/mobile/apps/client/maestro/scripts/generate_order_name.js create mode 100644 apps/mobile/apps/staff/maestro/profile/happy_path/persistent_profile_edit_e2e.yaml create mode 100644 apps/mobile/apps/staff/maestro/shifts/confirm_booking_dialog.yaml create mode 100644 apps/mobile/apps/staff/maestro/shifts/happy_path/geofence_clock_in_e2e.yaml diff --git a/apps/mobile/apps/client/maestro/billing/happy_path/comprehensive_billing_flow.yaml b/apps/mobile/apps/client/maestro/billing/happy_path/comprehensive_billing_flow.yaml new file mode 100644 index 00000000..f36983e6 --- /dev/null +++ b/apps/mobile/apps/client/maestro/billing/happy_path/comprehensive_billing_flow.yaml @@ -0,0 +1,62 @@ +# Client App — E2E: Comprehensive Invoice Approval +# Purpose: +# - Navigates to Billing -> Awaiting Approval. +# - Captures the Invoice ID or date/amount (conceptually). +# - Approves the invoice. +# - Verifies success message. +# - Navigates to the "Approved" tab. +# - Verifies the invoice is now listed in the history. +# +# Run: +# maestro test \ +11: # apps/mobile/apps/client/maestro/auth/happy_path/sign_in.yaml \ +12: # apps/mobile/apps/client/maestro/billing/happy_path/comprehensive_billing_flow.yaml \ +13: # -e TEST_CLIENT_EMAIL=... \ +14: # -e TEST_CLIENT_PASSWORD=... + +appId: com.krowwithus.client +--- +- launchApp: + clearState: false + +- tapOn: "Home" +- tapOn: "Billing" + +- extendedWaitUntil: + visible: "Awaiting Approval" + timeout: 10000 + +# 1. Enter the approval flow +- tapOn: "Awaiting Approval" + +- extendedWaitUntil: + visible: "Review & Approve" + timeout: 10000 + +# 2. Approve the first invoice +- tapOn: "Review & Approve" + +- extendedWaitUntil: + visible: "Approve" + timeout: 10000 + +- tapOn: "Approve" + +# 3. Verify success message +- extendedWaitUntil: + visible: "Invoice approved and payment initiated" + timeout: 15000 + +# 4. Deep Verification: check the 'Approved' tab +- tapOn: "Approved" + +- extendedWaitUntil: + visible: "Invoice History" + timeout: 10000 + +# Ensure we see a 'View Receipt' or 'Approved' status instead of 'Review' +- assertVisible: "View Receipt" +- assertNotVisible: "Review & Approve" + +# Back to Home +- tapOn: "Home" diff --git a/apps/mobile/apps/client/maestro/hubs/happy_path/hub_management_lifecycle_e2e.yaml b/apps/mobile/apps/client/maestro/hubs/happy_path/hub_management_lifecycle_e2e.yaml new file mode 100644 index 00000000..e0b9c5eb --- /dev/null +++ b/apps/mobile/apps/client/maestro/hubs/happy_path/hub_management_lifecycle_e2e.yaml @@ -0,0 +1,54 @@ +# Client App — E2E: Hub Management Lifecycle +# Purpose: +# - Create a new Hub. +# - Find and Edit the Hub. +# - Delete the Hub. +# - Verify it is gone from the list. + +appId: com.krowwithus.client +--- +- launchApp: + clearState: false + +- tapOn: "Home" +- tapOn: "Settings" # Assuming Hubs are managed via Settings or a Hubs tab + +# 1. Navigate to Hubs +- tapOn: "Manage Hubs" + +# 2. Create Hub +- tapOn: "Add New Hub" # Or the '+' button +- tapOn: "HUB NAME" +- inputText: "Maestro Test Hub" +- tapOn: "ADDRESS" +- inputText: "123 Test Street, NYC" +- hideKeyboard + +- tapOn: "Save Hub" + +- extendedWaitUntil: + visible: "Hub created successfully" + timeout: 10000 + +# 3. Edit Hub +- tapOn: "Maestro Test Hub" +- tapOn: "HUB NAME" +- inputText: "Updated Maestro Hub" +- hideKeyboard +- tapOn: "Save Hub" + +- extendedWaitUntil: + visible: "Hub updated successfully" + timeout: 10000 + +# 4. Delete Hub +- tapOn: "Updated Maestro Hub" +- tapOn: "Delete Hub" +- tapOn: "DELETE" # Confirmation + +- extendedWaitUntil: + visible: "Hub deleted" + timeout: 10000 + +# 5. Verify gone +- assertNotVisible: "Updated Maestro Hub" diff --git a/apps/mobile/apps/client/maestro/orders/debug/staff_search_only.yaml b/apps/mobile/apps/client/maestro/orders/debug/staff_search_only.yaml new file mode 100644 index 00000000..25b42a07 --- /dev/null +++ b/apps/mobile/apps/client/maestro/orders/debug/staff_search_only.yaml @@ -0,0 +1,85 @@ +# DEBUG: Staff Search Only +# Tests ONLY the Staff shift search flow in isolation. +# Pass a known order name to verify the Staff app can find it. +# +# Usage: +# maestro test staff_search_only.yaml -e TEST_STAFF_PHONE="5557654321" -e TEST_STAFF_OTP="123456" -e TEST_ORDER_NAME="E2E-XXXXX" + +appId: com.krowwithus.staff +--- +- launchApp: + clearState: false + +# Wait for app to fully render +- waitForAnimationToEnd: + timeout: 15000 + +# Sign in if needed +- runFlow: + when: + visible: "Log In" + file: ../../../../staff/maestro/auth/happy_path/sign_in.yaml + env: + TEST_STAFF_PHONE: ${TEST_STAFF_PHONE} + TEST_STAFF_OTP: ${TEST_STAFF_OTP} + +# Navigate to Shifts +- tapOn: + id: "nav_shifts" + +# Pull to refresh (force reload from backend) +- swipe: + start: 50%, 30% + end: 50%, 80% +- waitForAnimationToEnd: + timeout: 8000 + +# 3. Aggressive Retry Loop: Search, if not found, refresh and try again. +# This handles slow backend indexing by periodically pulling fresh data. +- repeat: + times: 5 + commands: + - runFlow: + when: + notVisible: + id: "shft_card_logo_placeholder" + index: 0 + commands: + # 1. Clear current search + - tapOn: + id: "find_shifts_search_input" + - waitForAnimationToEnd: + timeout: 2000 + - inputText: "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b" # Strategic clear if "" fails + - inputText: "" + - tapOn: "Shifts" # Dismiss keyboard + + # 2. Pull-to-refresh + - swipe: + start: 50%, 60% + end: 50%, 90% + - waitForAnimationToEnd: + timeout: 15000 + + # 3. Search again + - tapOn: + id: "find_shifts_search_input" + - waitForAnimationToEnd: + timeout: 2000 + - inputText: "${TEST_ORDER_NAME}\n" + - tapOn: "Shifts" # Dismiss keyboard + - waitForAnimationToEnd: + timeout: 5000 + +# Final check with a long timeout +- extendedWaitUntil: + visible: + id: "shft_card_logo_placeholder" + index: 0 + timeout: 30000 + +- assertVisible: + id: "shft_card_logo_placeholder" + index: 0 + + diff --git a/apps/mobile/apps/client/maestro/orders/happy_path/comprehensive_order_lifecycle.yaml b/apps/mobile/apps/client/maestro/orders/happy_path/comprehensive_order_lifecycle.yaml new file mode 100644 index 00000000..6646ad25 --- /dev/null +++ b/apps/mobile/apps/client/maestro/orders/happy_path/comprehensive_order_lifecycle.yaml @@ -0,0 +1,118 @@ +# Client App — E2E: Comprehensive Order Lifecycle (Create -> Verify in List -> View Details) +# Purpose: +# - Generates a unique order name via JS. +# - Creates a One-Time order. +# - Verifies the success confirmation. +# - Navigates to the Orders list. +# - Uses scrollUntilVisible to find the unique order. +# - Taps the order and verifies details on the summary page. +# +# Run: +# maestro test \ +11: # apps/mobile/apps/client/maestro/auth/happy_path/sign_in.yaml \ +12: # apps/mobile/apps/client/maestro/orders/happy_path/comprehensive_order_lifecycle.yaml \ +13: # -e TEST_CLIENT_EMAIL=... \ +14: # -e TEST_CLIENT_PASSWORD=... + +appId: com.krowwithus.client +--- +- launchApp: + clearState: false + +# 1. Generate unique order name +- runScript: ../../scripts/generate_order_name.js + +- tapOn: "Home" +- extendedWaitUntil: + visible: "Welcome back" + timeout: 15000 + +# 2. Start Create Order Flow +- tapOn: "Create Order\\nSchedule shifts" + +- extendedWaitUntil: + visible: "One-Time\\nSingle Event or Shift Request" + timeout: 10000 + +- tapOn: "One-Time\\nSingle Event or Shift Request" + +- extendedWaitUntil: + visible: "One-Time Order" + timeout: 10000 + +# 3. Fill Order Details +- tapOn: ".*(ORDER NAME|EVENT NAME|Order[ Nn]ame|Event[ Nn]ame).*" +- inputText: ${output.orderName} +- hideKeyboard + +# Select Role +- tapOn: ".*(Select Role|SELECT ROLE).*" +- tapOn: ".*\\$.*" # Just tap the first role from the dropdown + +# Set Start Time (Required for valid form) +- scrollUntilVisible: + element: "--:--" + direction: DOWN + timeout: 5000 + optional: true + +- tapOn: "--:--" +- extendedWaitUntil: + visible: "(?i)ok" + timeout: 5000 +- tapOn: "(?i)ok" + +# Set End Time (Required for valid form) +- tapOn: "--:--" +- extendedWaitUntil: + visible: "(?i)ok" + timeout: 5000 +- tapOn: "(?i)ok" + +# Scroll if needed to see the Create Order button +- scrollUntilVisible: + element: "(?i)Create Order" + direction: DOWN + timeout: 5000 + +- tapOn: "(?i)Create Order" + +# 4. Verify Success Confirmation +- extendedWaitUntil: + visible: "(?i)Order Created.*" + timeout: 30000 + +- assertVisible: "(?i)Order Created.*" +- assertVisible: ${output.orderName} + +# 5. Navigate to Orders List and Verify Persistence +- tapOn: "Back to Orders" + +- extendedWaitUntil: + visible: "Orders" + timeout: 15000 + +# Select the "Up Next" or "Active" tab if not already selected (assuming default is correct) +- tapOn: "Up Next" + +# Scroll until we find our unique order name +- scrollUntilVisible: + element: ${output.orderName} + direction: DOWN + timeout: 20000 + +- assertVisible: ${output.orderName} + +# 6. View Details and Final Verification +- tapOn: ${output.orderName} + +- extendedWaitUntil: + visible: "Order Summary" + timeout: 10000 + +- assertVisible: ${output.orderName} +- assertVisible: "POSTED" # or whichever status it initialises to +- assertVisible: "Role" # Check if role label is visible + +# Optionally, go back to Home +- tapOn: "Home" diff --git a/apps/mobile/apps/client/maestro/orders/happy_path/create_order_one_time_e2e.yaml b/apps/mobile/apps/client/maestro/orders/happy_path/create_order_one_time_e2e.yaml index b12933d1..477d408b 100644 --- a/apps/mobile/apps/client/maestro/orders/happy_path/create_order_one_time_e2e.yaml +++ b/apps/mobile/apps/client/maestro/orders/happy_path/create_order_one_time_e2e.yaml @@ -33,24 +33,54 @@ appId: com.krowwithus.client visible: "One-Time Order" timeout: 10000 -# Event Name (ORDER NAME) -- tapOn: "ORDER NAME" +# Wait for form or empty state data to load from API +- extendedWaitUntil: + visible: ".*(Create your order|No Vendors Available).*" + timeout: 15000 + +- assertNotVisible: "No Vendors Available" +- tapOn: ".*(ORDER NAME|EVENT NAME|Order[ Nn]ame|Event[ Nn]ame).*" - inputText: "Test E2E Event" - hideKeyboard # Wait for Vendor and Hub to auto-populate the defaults from the API. # We just need to give it a second. - extendedWaitUntil: - visible: "Select Role" + visible: ".*(Select Role|SELECT ROLE).*" timeout: 10000 # Select Role (Required for valid form) -- tapOn: "Select Role" -- tapOn: - point: "50%,50%" # Just tap the first role from the dropdown +- tapOn: ".*(Select Role|SELECT ROLE).*" +- tapOn: ".*\\$.*" # Tap the first role from the dropdown + +# Set Start Time (Required for valid form) +- scrollUntilVisible: + element: "--:--" + direction: DOWN + timeout: 5000 + optional: true + +- tapOn: "--:--" +- extendedWaitUntil: + visible: "(?i)ok" + timeout: 5000 +- tapOn: "(?i)ok" + +# Set End Time (Required for valid form) +- tapOn: "--:--" +- extendedWaitUntil: + visible: "(?i)ok" + timeout: 5000 +- tapOn: "(?i)ok" + +- scrollUntilVisible: + element: "(?i)Create Order" + direction: DOWN + timeout: 5000 + optional: true # Wait for Create Order button to be enabled (isValid=true handles this by making it clickable) -- tapOn: "Create Order" +- tapOn: "(?i)Create Order" # Success screen shows "Order received." or similar success title/message - extendedWaitUntil: diff --git a/apps/mobile/apps/client/maestro/orders/happy_path/cross_app_order_verification.yaml b/apps/mobile/apps/client/maestro/orders/happy_path/cross_app_order_verification.yaml new file mode 100644 index 00000000..5c9e1144 --- /dev/null +++ b/apps/mobile/apps/client/maestro/orders/happy_path/cross_app_order_verification.yaml @@ -0,0 +1,180 @@ +# Multi-App E2E: Client Creates Order -> Staff Sees Shift +# Purpose: +# - Historically verifies that an order created in the Client app +# is instantly visible as a shift in the Staff app. +# +# Run: +# maestro test cross_app_order_verification.yaml \ +11: # -e TEST_CLIENT_EMAIL=... \ +12: # -e TEST_CLIENT_PASSWORD=... \ +13: # -e TEST_STAFF_PHONE=... \ +14: # -e TEST_STAFF_OTP=... + +appId: com.krowwithus.client +--- +# --- PHASE 1: CLIENT CREATES ORDER --- +- launchApp: + clearState: false + +- runScript: ../../scripts/generate_order_name.js + +- tapOn: + text: "(?i)Home" + optional: true +- extendedWaitUntil: + visible: "(?i)Welcome back" + timeout: 30000 + +- tapOn: "Create Order\nSchedule shifts" +- tapOn: "One-Time\nSingle Event or Shift Request" + +- extendedWaitUntil: + visible: "One-Time Order" + timeout: 10000 + + # 3. Fill Order Details +- tapOn: ".*(ORDER NAME|EVENT NAME|Order[ Nn]ame|Event[ Nn]ame).*" +- inputText: ${output.orderName} +- hideKeyboard + +# Select Role +- tapOn: ".*(Select Role|SELECT ROLE).*" +- tapOn: ".*\\$.*" + +# Set Start Time (Required for valid form) +- scrollUntilVisible: + element: "--:--" + direction: DOWN + timeout: 5000 + optional: true + +- tapOn: "--:--" +- extendedWaitUntil: + visible: "(?i)ok" + timeout: 5000 +- tapOn: "(?i)ok" + +# Set End Time (Required for valid form) +- tapOn: "--:--" +- extendedWaitUntil: + visible: "(?i)ok" + timeout: 5000 +- tapOn: "(?i)ok" + +- scrollUntilVisible: + element: "(?i)Create Order" + direction: DOWN + +- tapOn: "(?i)Create Order" + +# 4. Verify Success Confirmation +- extendedWaitUntil: + visible: "(?i)Order Created.*" + timeout: 30000 + +- stopApp: com.krowwithus.client + +# --- PHASE 2: STAFF VERIFIES SHIFT --- +- appId: com.krowwithus.staff +- launchApp: + clearState: false + +# If not logged in, we'd need a sign_in flow here, but we assume state is kept +# or we run this after a staff sign-in test. +# For a standalone test, we should include the login steps. + +# (Optional login steps if app starts at landing) +- runFlow: + when: + visible: "Join the Workforce" + file: ../../../../staff/maestro/auth/happy_path/sign_in.yaml + env: + PHONE: ${TEST_STAFF_PHONE} + OTP: ${TEST_STAFF_OTP} + +- extendedWaitUntil: + visible: "Shifts" + timeout: 20000 + +- launchApp: + appId: com.krowwithus.staff + clearState: false + +# Wait for Staff app to fully render AND give backend time to index the new order +- waitForAnimationToEnd: + timeout: 30000 + +# 1. Navigate to Shifts tab +- tapOn: + id: "nav_shifts" + +# 2. Refresh #1: Swipe to force backend reload, THEN search +- swipe: + start: 50%, 30% + end: 50%, 80% +- waitForAnimationToEnd: + timeout: 8000 + +# Wait for the search field to appear (visible when Find Shifts tab is active) +- extendedWaitUntil: + visible: "(?i)Search jobs.*" + timeout: 15000 + +# 3. Aggressive Retry Loop: Search, if not found, refresh and try again. +- repeat: + times: 5 + commands: + - runFlow: + when: + notVisible: + id: "shft_card_logo_placeholder" + index: 0 + commands: + # 1. Clear current search + - tapOn: + id: "find_shifts_search_input" + - waitForAnimationToEnd: + timeout: 2000 + - inputText: "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b" + - inputText: "" + - tapOn: "Shifts" # Dismiss keyboard + + # 2. Pull-to-refresh + - swipe: + start: 50%, 60% + end: 50%, 90% + - waitForAnimationToEnd: + timeout: 15000 + + # 3. Enter search query again + - tapOn: + id: "find_shifts_search_input" + - waitForAnimationToEnd: + timeout: 2000 + - inputText: "${output.orderName}\n" + - tapOn: "Shifts" # Dismiss keyboard + - waitForAnimationToEnd: + timeout: 5000 + +# 4. Final wait — plenty of time for slow backends +- extendedWaitUntil: + visible: + id: "shft_card_logo_placeholder" + index: 0 + timeout: 60000 + +- assertVisible: + id: "shft_card_logo_placeholder" + index: 0 + +- tapOn: + id: "shft_card_logo_placeholder" + index: 0 + +# Wait for Details page with a very generous timeout for slow backend syncing +- extendedWaitUntil: + visible: "LOCATION" + timeout: 45000 + +- assertVisible: "${output.orderName}" +- assertVisible: "(?i)(Apply|Accept|Clock|Confirm).*" diff --git a/apps/mobile/apps/client/maestro/orders/happy_path/full_lifecycle_multiapp.yaml b/apps/mobile/apps/client/maestro/orders/happy_path/full_lifecycle_multiapp.yaml new file mode 100644 index 00000000..e85ba04e --- /dev/null +++ b/apps/mobile/apps/client/maestro/orders/happy_path/full_lifecycle_multiapp.yaml @@ -0,0 +1,272 @@ +# Multi-App E2E: Full Order-to-Payment Lifecycle +# Roles: Client, Staff +# Flow: Create -> Apply -> Clock In (Geofence) -> Clock Out -> Approve Payment + +appId: com.krowwithus.client # Starts with Client profile +--- +# === PERSONA: CLIENT === +- launchApp: + clearState: false + +# 1. Generate unique identifier for this session +- runScript: ../../scripts/generate_order_name.js + +# 2. Create the Order +- waitForAnimationToEnd: + timeout: 10000 +- tapOn: + text: "(?i)Home" + optional: true +- extendedWaitUntil: + visible: "(?i)Welcome back" + timeout: 30000 + +- tapOn: "Create Order\\nSchedule shifts" + +- extendedWaitUntil: + visible: "One-Time\\nSingle Event or Shift Request" + timeout: 10000 + +- tapOn: "One-Time\\nSingle Event or Shift Request" + +- extendedWaitUntil: + visible: "One-Time Order" + timeout: 15000 + +- extendedWaitUntil: + visible: ".*(Create your order|No Vendors Available).*" + timeout: 15000 + +# Safety check: ensure the account has vendors setup! +- assertNotVisible: "No Vendors Available" + +- tapOn: ".*(ORDER NAME|EVENT NAME|Order[ Nn]ame|Event[ Nn]ame).*" +- inputText: "${output.orderName}" +- hideKeyboard + +# Select a Hub (assuming first one is auto-selected or we tap) +- tapOn: + text: ".*(HUB|Hub).*" + optional: true + +# Wait for Vendor and Hub to auto-populate the defaults from the API. +- extendedWaitUntil: + visible: ".*(Select Role|SELECT ROLE).*" + timeout: 10000 + +# Select Role (Required for valid form) +- tapOn: ".*(Select Role|SELECT ROLE).*" +- tapOn: ".*\\$.*" # Tap the first role from the dropdown (matches 'Role - $Cost') + +# Set Start Time (Required for valid form) +- scrollUntilVisible: + element: "--:--" + direction: DOWN + timeout: 5000 + optional: true + +- tapOn: "--:--" +- extendedWaitUntil: + visible: "(?i)ok" + timeout: 5000 +- tapOn: "(?i)ok" + +# Set End Time (Required for valid form) +- tapOn: "--:--" +- extendedWaitUntil: + visible: "(?i)ok" + timeout: 5000 +- tapOn: "(?i)ok" + +- scrollUntilVisible: + element: "(?i)Create Order" + direction: DOWN + timeout: 5000 + optional: true + +- tapOn: "(?i)Create Order" + +- extendedWaitUntil: + visible: "(?i)Order Created.*" + timeout: 30000 + +# Cool-down to let backend index the new order +- waitForAnimationToEnd: + timeout: 35000 + +- stopApp + +# === PERSONA: STAFF === +- launchApp: + appId: com.krowwithus.staff + clearState: false + +# Wait for Staff app to fully render (also serves as additional backend indexing time) +- waitForAnimationToEnd: + timeout: 15000 + +# Sign in if needed +- runFlow: + when: + visible: "Log In" + file: ../../../../staff/maestro/auth/happy_path/sign_in.yaml + env: + TEST_STAFF_PHONE: ${TEST_STAFF_PHONE} + TEST_STAFF_OTP: ${TEST_STAFF_OTP} + +# 1. Navigate to Shifts tab +- tapOn: + id: "nav_shifts" + +# 2. Refresh #1: Swipe to force backend reload, THEN search +- swipe: + start: 50%, 30% + end: 50%, 80% +- waitForAnimationToEnd: + timeout: 8000 + +# Wait for the search field to appear (visible when Find Shifts tab is active) +- extendedWaitUntil: + visible: "(?i)Search jobs.*" + timeout: 15000 + +# 3. Aggressive Retry Loop: Search, if not found, refresh and try again. +- repeat: + times: 5 + commands: + - runFlow: + when: + notVisible: + id: "shft_card_logo_placeholder" + index: 0 # Index 0 for Logo (ID is unique to cards) + commands: + # 1. Clear current search + - tapOn: + id: "find_shifts_search_input" + - waitForAnimationToEnd: + timeout: 2000 + - inputText: "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b" + - inputText: "" + - tapOn: "Shifts" # Dismiss keyboard + + # 2. Pull-to-refresh + - swipe: + start: 50%, 60% + end: 50%, 90% + - waitForAnimationToEnd: + timeout: 15000 + + # 3. Enter search query again + - tapOn: + id: "find_shifts_search_input" + - waitForAnimationToEnd: + timeout: 2000 + - inputText: "${output.orderName}\n" + - tapOn: "Shifts" # Dismiss keyboard + - waitForAnimationToEnd: + timeout: 5000 + +# 4. Final wait — plenty of time for slow backends +- extendedWaitUntil: + visible: + id: "shft_card_logo_placeholder" + index: 0 + timeout: 60000 + +- tapOn: + id: "shft_card_logo_placeholder" + index: 0 + +# Wait for Details page with a very generous timeout for slow backend syncing +- extendedWaitUntil: + visible: "LOCATION" + timeout: 45000 + +# 2. Book the shift (Instant Book flow) +- extendedWaitUntil: + visible: "(?i)(Apply|Accept|Clock|Confirm).*" + timeout: 20000 + +- tapOn: "(?i)(Apply|Accept|Clock|Confirm).*" + +# Handle confirmation dialog if it appears +- runFlow: + when: + visible: "(?i)Book Shift" + file: ../../../../staff/maestro/shifts/confirm_booking_dialog.yaml + +- extendedWaitUntil: + visible: "Shift successfully booked!" + timeout: 20000 + +- extendedWaitUntil: + visible: "(?i)Welcome back" + timeout: 20000 + +# Tap Home tab to reset navigation context +- tapOn: + id: "nav_home" + +# Tap Clock In tab +- tapOn: + id: "nav_clock_in" + +# Wait for Clock In screen to load +- extendedWaitUntil: + visible: "(?i)Clock In to your Shift" + timeout: 15000 + +# Set location to the Venue (NYC Grand Hotel as per ClockInCubit) +- setLocation: + latitude: 40.7128 + longitude: -74.0060 + +- extendedWaitUntil: + visible: "Swipe to Check In" + timeout: 15000 + +- swipe: + direction: RIGHT + element: "Swipe to Check In" + +- extendedWaitUntil: + visible: "Check In!" + timeout: 15000 + +# 4. Clock Out (immediately for test purposes) +- swipe: + direction: RIGHT + element: "Swipe to Check Out" + +- extendedWaitUntil: + visible: "Check Out!" # Assuming similar success UI + timeout: 10000 + optional: true + +- stopApp + +# === PERSONA: CLIENT === +- launchApp: + appId: com.krowwithus.client + clearState: false + +# 1. Verify and Approve Billing +- tapOn: "Billing" +- tapOn: "Awaiting Approval" + +# Find the specific approved shift's related invoice +# For the test, we assume the top one is our most recent completion +- extendedWaitUntil: + visible: "Review & Approve" + timeout: 10000 + +- tapOn: "Review & Approve" + +- tapOn: "Approve" + +- extendedWaitUntil: + visible: "Invoice approved" + timeout: 15000 + +# Test complete - invoice approved successfully +- assertVisible: "Invoice approved" diff --git a/apps/mobile/apps/client/maestro/orders/happy_path/reorder_historical_data_e2e.yaml b/apps/mobile/apps/client/maestro/orders/happy_path/reorder_historical_data_e2e.yaml new file mode 100644 index 00000000..069daa36 --- /dev/null +++ b/apps/mobile/apps/client/maestro/orders/happy_path/reorder_historical_data_e2e.yaml @@ -0,0 +1,53 @@ +# Client App — E2E: Reorder Flow +# Purpose: +# - Navigates Home → Reorder Section. +# - Taps "Reorder" on a recent shift. +# - Verifies the One-Time Order form is correctly populated. +# - Submits the reordered shift. + +appId: com.krowwithus.client +--- +- launchApp: + clearState: false + +- tapOn: "Home" + +# 1. Generate a new name for the reorder to avoid confusion +- runScript: ../../scripts/generate_order_name.js + +# 2. Find the "Reorder" section (translates to "Recently Completed" or similar) +# We search for the Reorder button text +- scrollUntilVisible: + element: "REORDER" + direction: DOWN + timeout: 10000 + +- tapOn: "REORDER" + +# 3. Verification: We should be on the One-Time Order creation page +- extendedWaitUntil: + visible: "One-Time Order" + timeout: 10000 + +# 4. Verification: Some fields should be pre-filled (e.g. Hub) +# We can't easily check exact pre-fill values without knowing the history, +# but we can verify we are in the flow and the form is loaded. +- extendedWaitUntil: + visible: ".*(Create your order|No Vendors Available).*" + timeout: 15000 + +- assertNotVisible: "No Vendors Available" +- assertVisible: ".*(ORDER NAME|EVENT NAME|Order[ Nn]ame|Event[ Nn]ame).*" + +# 5. Overwrite name and Submit +- tapOn: ".*(ORDER NAME|EVENT NAME|Order[ Nn]ame|Event[ Nn]ame).*" +- inputText: "${output.orderName}" + +- hideKeyboard + +- tapOn: "(?i)Create Order" + +# 6. Success check +- extendedWaitUntil: + visible: "(?i)Order Created.*" + timeout: 15000 diff --git a/apps/mobile/apps/client/maestro/reports/happy_path/comprehensive_reports_verification.yaml b/apps/mobile/apps/client/maestro/reports/happy_path/comprehensive_reports_verification.yaml new file mode 100644 index 00000000..e6582d42 --- /dev/null +++ b/apps/mobile/apps/client/maestro/reports/happy_path/comprehensive_reports_verification.yaml @@ -0,0 +1,62 @@ +# Client App — E2E: Reports Insights Verification +# Purpose: +# - Navigates to Reports. +# - Opens Spend Report. +# - Verifies that charts and summary cards are loaded. +# - Opens No-Show Report. +# - Verifies the empty/data states are handled. + +appId: com.krowwithus.client +--- +- launchApp: + clearState: false + +- tapOn: "Home" +- tapOn: "Reports" + +# 1. Verify Spend Report +- extendedWaitUntil: + visible: "Spend Report" + timeout: 10000 + +- tapOn: "Spend Report" + +# Verify specialized summary cards are visible +- extendedWaitUntil: + visible: "TOTAL SPEND" + timeout: 10000 + +- assertVisible: "AVG DAILY COST" + +# Swipe to see historical chart content if needed +- swipe: + direction: DOWN + element: "TOTAL SPEND" + +- assertVisible: "SPEND BY INDUSTRY" + +# Go back +- tapOn: + id: "back_button" # Or the arrow icon + optional: true +- tapOn: + point: "8% 8%" # Fallback to top-left if ID missing + optional: true + +# 2. Verify No-Show Report +- extendedWaitUntil: + visible: "No-Show Report" + timeout: 10000 + +- tapOn: "No-Show Report" + +- extendedWaitUntil: + visible: "(?i).*(No-Show|Report).*" + timeout: 10000 + +# Smoke check for data availability +- assertVisible: + text: "No show incidents" + optional: true + +- tapOn: "Home" diff --git a/apps/mobile/apps/client/maestro/scripts/generate_order_name.js b/apps/mobile/apps/client/maestro/scripts/generate_order_name.js new file mode 100644 index 00000000..fe39001a --- /dev/null +++ b/apps/mobile/apps/client/maestro/scripts/generate_order_name.js @@ -0,0 +1 @@ +output.orderName = "E2E-" + Math.random().toString(36).substring(2, 7).toUpperCase(); diff --git a/apps/mobile/apps/staff/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/apps/mobile/apps/staff/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java index 14bcfe40..632797fd 100644 --- a/apps/mobile/apps/staff/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ b/apps/mobile/apps/staff/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -75,6 +75,11 @@ public final class GeneratedPluginRegistrant { } catch (Exception e) { Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e); } + try { + flutterEngine.getPlugins().add(new fman.ge.smart_auth.SmartAuthPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin smart_auth, fman.ge.smart_auth.SmartAuthPlugin", e); + } try { flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin()); } catch (Exception e) { diff --git a/apps/mobile/apps/staff/ios/Runner/GeneratedPluginRegistrant.m b/apps/mobile/apps/staff/ios/Runner/GeneratedPluginRegistrant.m index 72d03754..359520a4 100644 --- a/apps/mobile/apps/staff/ios/Runner/GeneratedPluginRegistrant.m +++ b/apps/mobile/apps/staff/ios/Runner/GeneratedPluginRegistrant.m @@ -66,6 +66,12 @@ @import shared_preferences_foundation; #endif +#if __has_include() +#import +#else +@import smart_auth; +#endif + #if __has_include() #import #else @@ -85,6 +91,7 @@ [PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]]; [RecordIosPlugin registerWithRegistrar:[registry registrarForPlugin:@"RecordIosPlugin"]]; [SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]]; + [SmartAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"SmartAuthPlugin"]]; [URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]]; } diff --git a/apps/mobile/apps/staff/linux/flutter/generated_plugin_registrant.cc b/apps/mobile/apps/staff/linux/flutter/generated_plugin_registrant.cc index adc5a9fe..e0201fc3 100644 --- a/apps/mobile/apps/staff/linux/flutter/generated_plugin_registrant.cc +++ b/apps/mobile/apps/staff/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) record_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); record_linux_plugin_register_with_registrar(record_linux_registrar); + g_autoptr(FlPluginRegistrar) smart_auth_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SmartAuthPlugin"); + smart_auth_plugin_register_with_registrar(smart_auth_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/apps/mobile/apps/staff/linux/flutter/generated_plugins.cmake b/apps/mobile/apps/staff/linux/flutter/generated_plugins.cmake index 1262bd6f..777ef11b 100644 --- a/apps/mobile/apps/staff/linux/flutter/generated_plugins.cmake +++ b/apps/mobile/apps/staff/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux record_linux + smart_auth url_launcher_linux ) diff --git a/apps/mobile/apps/staff/macos/Flutter/GeneratedPluginRegistrant.swift b/apps/mobile/apps/staff/macos/Flutter/GeneratedPluginRegistrant.swift index e919f640..6cef433c 100644 --- a/apps/mobile/apps/staff/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/apps/mobile/apps/staff/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,6 +13,7 @@ import firebase_core import geolocator_apple import record_macos import shared_preferences_foundation +import smart_auth import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -24,5 +25,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SmartAuthPlugin.register(with: registry.registrar(forPlugin: "SmartAuthPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/apps/mobile/apps/staff/maestro/profile/happy_path/persistent_profile_edit_e2e.yaml b/apps/mobile/apps/staff/maestro/profile/happy_path/persistent_profile_edit_e2e.yaml new file mode 100644 index 00000000..6a7f9269 --- /dev/null +++ b/apps/mobile/apps/staff/maestro/profile/happy_path/persistent_profile_edit_e2e.yaml @@ -0,0 +1,44 @@ +# Staff App — E2E: Profile Update Verification +# Purpose: +# - Navigates to Profile -> Personal Info. +# - Updates the phone number. +# - Saves and verifies success snackbar. +# - Re-enters page to verify PERSISTENCE. + +appId: com.krowwithus.staff +--- +- launchApp: + clearState: false + +- tapOn: "Profile" +- tapOn: "Personal Information" + +# 1. Update Phone +- extendedWaitUntil: + visible: "PHONE NUMBER" + timeout: 10000 + +- tapOn: "PHONE NUMBER" +# Clear and input new dummy number +- inputText: "5550199" +- hideKeyboard + +- tapOn: "Save" + +# 2. Verify success +- extendedWaitUntil: + visible: "Personal details updated" + timeout: 10000 + +# 3. Verify PERSISTENCE (The "Really" Test) +# We stop and restart to ensure it's not just local state +- stopApp +- launchApp +- tapOn: "Profile" +- tapOn: "Personal Information" + +- extendedWaitUntil: + visible: "5550199" + timeout: 10000 + +- assertVisible: "5550199" diff --git a/apps/mobile/apps/staff/maestro/shifts/confirm_booking_dialog.yaml b/apps/mobile/apps/staff/maestro/shifts/confirm_booking_dialog.yaml new file mode 100644 index 00000000..34ed4aea --- /dev/null +++ b/apps/mobile/apps/staff/maestro/shifts/confirm_booking_dialog.yaml @@ -0,0 +1,4 @@ +appId: com.krowwithus.staff +--- +# Helper flow to confirm a booking dialog if it appears +- tapOn: "(?i)(Apply Now|Confirm|OK)" diff --git a/apps/mobile/apps/staff/maestro/shifts/happy_path/geofence_clock_in_e2e.yaml b/apps/mobile/apps/staff/maestro/shifts/happy_path/geofence_clock_in_e2e.yaml new file mode 100644 index 00000000..72658487 --- /dev/null +++ b/apps/mobile/apps/staff/maestro/shifts/happy_path/geofence_clock_in_e2e.yaml @@ -0,0 +1,59 @@ +# Staff App — E2E: Geofence-Aware Clock-In +# Purpose: +# - Verifies that Clock-In is BLOCKED when staff is $>500m$ from venue. +# - Verifies that Clock-In is ENABLED when staff is within geofence. +# - Note: Uses Grand Hotel, NYC (40.7128, -74.0060) from ClockInCubit. + +appId: com.krowwithus.staff +--- +- launchApp: + clearState: false + +- tapOn: "Home" +- tapOn: "Clock In" + +# 1. TEST: OUT OF RANGE (London) +- setLocation: + latitude: 51.5074 + longitude: -0.1278 + +- extendedWaitUntil: + visible: "Clock In to your Shift" + timeout: 10000 + +# Tap search/refresh logic if needed, but usually Cubit triggers on entry +- extendedWaitUntil: + visible: ".*away.*must be within 500m.*" + timeout: 15000 + +- assertVisible: ".*away.*must be within 500m.*" +- assertNotVisible: "Swipe to Check In" + +# 2. TEST: IN RANGE (NYC Grand Hotel) +- setLocation: + latitude: 40.7128 + longitude: -74.0060 + +# Give the app a moment to refresh location (or tap a refresh button if implemented) +# Assuming the Cubit polls or we re-enter the page. +- stopApp +- launchApp +- tapOn: "Clock In" + +- extendedWaitUntil: + visible: "Swipe to Check In" + timeout: 15000 + +- assertVisible: "Swipe to Check In" +- assertNotVisible: ".*away.*must be within 500m.*" + +# 3. COMPLETE CLOCK IN +- swipe: + direction: RIGHT + element: "Swipe to Check In" + +- extendedWaitUntil: + visible: "Check In!" + timeout: 15000 + +- assertVisible: "Swipe to Check Out" diff --git a/apps/mobile/apps/staff/windows/flutter/generated_plugin_registrant.cc b/apps/mobile/apps/staff/windows/flutter/generated_plugin_registrant.cc index b6746a97..e810dd28 100644 --- a/apps/mobile/apps/staff/windows/flutter/generated_plugin_registrant.cc +++ b/apps/mobile/apps/staff/windows/flutter/generated_plugin_registrant.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -27,6 +28,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); RecordWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); + SmartAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SmartAuthPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/apps/mobile/apps/staff/windows/flutter/generated_plugins.cmake b/apps/mobile/apps/staff/windows/flutter/generated_plugins.cmake index 589f702c..8eade70f 100644 --- a/apps/mobile/apps/staff/windows/flutter/generated_plugins.cmake +++ b/apps/mobile/apps/staff/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST geolocator_windows permission_handler_windows record_windows + smart_auth url_launcher_windows ) diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart b/apps/mobile/packages/data_connect/lib/src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart index cb760a6f..e8513f1d 100644 --- a/apps/mobile/packages/data_connect/lib/src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart +++ b/apps/mobile/packages/data_connect/lib/src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart @@ -1,4 +1,4 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs +// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:intl/intl.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; @@ -109,11 +109,13 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository { endTime: endTime, ); + final String title = sr.role.name; + mappedShifts.add( Shift( id: sr.shiftId, roleId: sr.roleId, - title: sr.role.name, + title: title, clientName: sr.shift.order.business.businessName, logoUrl: null, hourlyRate: sr.role.costPerHour, @@ -213,10 +215,12 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository { status = _mapApplicationStatus(s); } + final String title = sr.role.name; + return Shift( id: sr.shiftId, roleId: sr.roleId, - title: sr.shift.order.business.businessName, + title: title, clientName: sr.shift.order.business.businessName, logoUrl: sr.shift.order.business.companyLogoUrl, hourlyRate: sr.role.costPerHour, diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart index 8bb83203..b5d2014e 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart @@ -255,6 +255,7 @@ class _ShiftOrderFormSheetState extends State { final fdc.OperationResult shiftResult = await _dataConnect .createShift(title: shiftTitle, orderId: orderId) + .description(_orderNameController.text) .date(orderTimestamp) .location(selectedHub.hubName) .locationAddress(selectedHub.address) diff --git a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart index 2fe608d0..90d1dd55 100644 --- a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart +++ b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart @@ -46,6 +46,7 @@ class _OneTimeOrderEventNameInputState @override Widget build(BuildContext context) { return UiTextField( + semanticsIdentifier: 'order_name_input', label: widget.label, controller: _controller, onChanged: widget.onChanged, diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart index d6f85fe5..4e6093be 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:core_localization/core_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinput/pinput.dart'; +import 'package:smart_auth/smart_auth.dart'; import '../../../blocs/auth_event.dart'; import '../../../blocs/auth_bloc.dart'; @@ -29,144 +31,98 @@ class OtpInputField extends StatefulWidget { } class _OtpInputFieldState extends State { - final List _controllers = List.generate( - 6, - (int _) => TextEditingController(), - ); - final List _focusNodes = List.generate(6, (int _) => FocusNode()); - - /// Hidden field for E2E: Maestro inputText sends full OTP in one call; - /// the 6 visible boxes have maxLength:1 and would truncate. - late final FocusNode _hiddenFocusNode; + late final TextEditingController _controller; + late final SmartAuth _smartAuth; @override void initState() { super.initState(); - _hiddenFocusNode = FocusNode(); + _controller = TextEditingController(); + _smartAuth = SmartAuth(); + _listenForSmsCode(); } @override void dispose() { - _hiddenFocusNode.dispose(); - for (final TextEditingController controller in _controllers) { - controller.dispose(); - } - for (final FocusNode node in _focusNodes) { - node.dispose(); - } + _controller.dispose(); super.dispose(); } - /// Distributes full OTP from hidden field to the 6 visible boxes and notifies Bloc. - void _syncFromHidden(BuildContext context, String value) { - final String raw = value.replaceAll(RegExp(r'\D'), ''); - final String digits = raw.length > 6 ? raw.substring(0, 6) : raw; - for (int i = 0; i < 6; i++) { - final String digit = i < digits.length ? digits[i] : ''; - if (_controllers[i].text != digit) { - _controllers[i].text = digit; - } - } - BlocProvider.of(context).add(AuthOtpUpdated(digits)); - if (digits.length == 6) { - widget.onCompleted(digits); + Future _listenForSmsCode() async { + final res = await _smartAuth.getSmsCode(); + if (res.code != null && mounted) { + _controller.text = res.code!; + _onChanged(_controller.text); } } - /// Helper getter to compute the current OTP code from all controllers. - String get _otpCode => _controllers.map((TextEditingController c) => c.text).join(); - - /// Handles changes to the OTP input fields. - void _onChanged({ - required BuildContext context, - required int index, - required String value, - }) { - if (value.length == 1 && index < 5) { - _focusNodes[index + 1].requestFocus(); - } - + void _onChanged(String value) { // Notify the Bloc of the change - BlocProvider.of(context).add(AuthOtpUpdated(_otpCode)); + BlocProvider.of(context).add(AuthOtpUpdated(value)); - if (_otpCode.length == 6) { - widget.onCompleted(_otpCode); + if (value.length == 6) { + widget.onCompleted(value); } } @override Widget build(BuildContext context) { - const double boxWidth = 45; - const double boxHeight = 56; + final defaultPinTheme = PinTheme( + width: 45, + height: 56, + textStyle: UiTypography.headline3m, + decoration: BoxDecoration( + borderRadius: UiConstants.radiusMd, + border: Border.all( + color: widget.error.isNotEmpty ? UiColors.textError : UiColors.border, + width: 2, + ), + ), + ); + + final focusedPinTheme = defaultPinTheme.copyWith( + decoration: defaultPinTheme.decoration!.copyWith( + border: Border.all( + color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary, + width: 2, + ), + ), + ); + + final submittedPinTheme = defaultPinTheme.copyWith( + decoration: defaultPinTheme.decoration!.copyWith( + border: Border.all( + color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary, + width: 2, + ), + ), + ); + + final errorPinTheme = defaultPinTheme.copyWith( + decoration: defaultPinTheme.decoration!.copyWith( + border: Border.all(color: UiColors.textError, width: 2), + ), + ); + return Column( children: [ SizedBox( width: 300, - height: boxHeight, - child: Stack( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate(6, (int index) { - final TextField field = TextField( - controller: _controllers[index], - focusNode: _focusNodes[index], - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - textAlign: TextAlign.center, - maxLength: 1, - style: UiTypography.headline3m, - decoration: InputDecoration( - counterText: '', - border: OutlineInputBorder( - borderSide: BorderSide( - color: widget.error.isNotEmpty - ? UiColors.textError - : (_controllers[index].text.isNotEmpty - ? UiColors.primary - : UiColors.border), - width: 2, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: UiConstants.radiusMd, - borderSide: BorderSide( - color: widget.error.isNotEmpty - ? UiColors.textError - : (_controllers[index].text.isNotEmpty - ? UiColors.primary - : UiColors.border), - width: 2, - ), - ), - ), - onChanged: (String value) => - _onChanged(context: context, index: index, value: value), - ); - return SizedBox(width: boxWidth, height: boxHeight, child: field); - }), - ), - Positioned.fill( - child: Semantics( - identifier: 'staff_otp_input', - container: false, - child: Opacity( - opacity: 0.01, - child: TextField( - focusNode: _hiddenFocusNode, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter(6), - ], - maxLength: 6, - onChanged: (String value) => - _syncFromHidden(context, value), - ), - ), - ), - ), - ], + child: Semantics( + identifier: 'staff_otp_input', + child: Pinput( + length: 6, + controller: _controller, + defaultPinTheme: defaultPinTheme, + focusedPinTheme: focusedPinTheme, + submittedPinTheme: submittedPinTheme, + errorPinTheme: errorPinTheme, + followingPinTheme: defaultPinTheme, + forceErrorState: widget.error.isNotEmpty, + onChanged: _onChanged, + onCompleted: widget.onCompleted, + autofillHints: const [AutofillHints.oneTimeCode], + ), ), ), if (widget.error.isNotEmpty) diff --git a/apps/mobile/packages/features/staff/authentication/pubspec.yaml b/apps/mobile/packages/features/staff/authentication/pubspec.yaml index 966934ef..81a6199f 100644 --- a/apps/mobile/packages/features/staff/authentication/pubspec.yaml +++ b/apps/mobile/packages/features/staff/authentication/pubspec.yaml @@ -18,6 +18,8 @@ dependencies: firebase_auth: ^6.1.2 firebase_data_connect: ^0.2.2+1 http: ^1.2.0 + pinput: ^5.0.0 + smart_auth: ^1.1.0 # Architecture Packages krow_domain: diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart index 6f6a3a6d..3a47e9ef 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart @@ -277,85 +277,89 @@ class _ShiftsPageState extends State { }) { final isActive = _activeTab == type; return Expanded( - child: GestureDetector( - onTap: !enabled - ? null - : () { - setState(() => _activeTab = type); - if (type == ShiftTabType.history) { - _bloc.add(LoadHistoryShiftsEvent()); - } - if (type == ShiftTabType.find) { - _bloc.add(LoadAvailableShiftsEvent()); - } - }, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: UiConstants.space2, - horizontal: UiConstants.space2, - ), - decoration: BoxDecoration( - color: isActive - ? UiColors.white - : UiColors.white.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - size: 14, - color: !enabled - ? UiColors.white.withValues(alpha: 0.5) - : isActive - ? UiColors.primary - : UiColors.white, - ), - const SizedBox(width: UiConstants.space1), - Flexible( - child: Text( - label, - style: - (isActive - ? UiTypography.body3m.copyWith( - color: UiColors.primary, - ) - : UiTypography.body3m.white) - .copyWith( - color: !enabled - ? UiColors.white.withValues(alpha: 0.5) - : null, - ), - overflow: TextOverflow.ellipsis, + child: Semantics( + identifier: 'shift_tab_${type.name}', + label: label, + child: GestureDetector( + onTap: !enabled + ? null + : () { + setState(() => _activeTab = type); + if (type == ShiftTabType.history) { + _bloc.add(LoadHistoryShiftsEvent()); + } + if (type == ShiftTabType.find) { + _bloc.add(LoadAvailableShiftsEvent()); + } + }, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space2, + horizontal: UiConstants.space2, + ), + decoration: BoxDecoration( + color: isActive + ? UiColors.white + : UiColors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(UiConstants.radiusMdValue), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 14, + color: !enabled + ? UiColors.white.withValues(alpha: 0.5) + : isActive + ? UiColors.primary + : UiColors.white, ), - ), - if (showCount) ...[ - const SizedBox(width: 4), - Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space1, - vertical: 2, + const SizedBox(width: UiConstants.space1), + Flexible( + child: Text( + label, + style: + (isActive + ? UiTypography.body3m.copyWith( + color: UiColors.primary, + ) + : UiTypography.body3m.white) + .copyWith( + color: !enabled + ? UiColors.white.withValues(alpha: 0.5) + : null, + ), + overflow: TextOverflow.ellipsis, ), - constraints: const BoxConstraints(minWidth: 18), - decoration: BoxDecoration( - color: isActive - ? UiColors.primary.withValues(alpha: 0.1) - : UiColors.white.withValues(alpha: 0.2), - borderRadius: UiConstants.radiusFull, - ), - child: Center( - child: Text( - "$count", - style: UiTypography.footnote1b.copyWith( - color: isActive ? UiColors.primary : UiColors.white, + ), + if (showCount) ...[ + const SizedBox(width: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space1, + vertical: 2, + ), + constraints: const BoxConstraints(minWidth: 18), + decoration: BoxDecoration( + color: isActive + ? UiColors.primary.withValues(alpha: 0.1) + : UiColors.white.withValues(alpha: 0.2), + borderRadius: UiConstants.radiusFull, + ), + child: Center( + child: Text( + "$count", + style: UiTypography.footnote1b.copyWith( + color: isActive ? UiColors.primary : UiColors.white, + ), ), ), ), - ), + ], ], - ], + ), ), ), ), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart index ad898f00..57976fdc 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart @@ -246,8 +246,10 @@ class _MyShiftCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Logo - Container( - width: 44, + Semantics( + identifier: 'shft_card_logo_placeholder', + child: Container( + width: 44, height: 44, decoration: BoxDecoration( gradient: LinearGradient( @@ -282,6 +284,7 @@ class _MyShiftCardState extends State { size: UiConstants.iconMd, ), ), + ), ), const SizedBox(width: UiConstants.space3), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart index a9468691..c6a67840 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart @@ -33,6 +33,7 @@ class _FindShiftsTabState extends State { String _jobType = 'all'; double? _maxDistance; // miles Position? _currentPosition; + final TextEditingController _searchController = TextEditingController(); @override void initState() { @@ -40,6 +41,12 @@ class _FindShiftsTabState extends State { _initLocation(); } + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + Future _initLocation() async { try { final LocationPermission permission = await Geolocator.checkPermission(); @@ -289,6 +296,7 @@ class _FindShiftsTabState extends State { final matchesSearch = s.title.toLowerCase().contains(_searchQuery.toLowerCase()) || s.location.toLowerCase().contains(_searchQuery.toLowerCase()) || + (s.description ?? '').toLowerCase().contains(_searchQuery.toLowerCase()) || s.clientName.toLowerCase().contains(_searchQuery.toLowerCase()); if (!matchesSearch) return false; @@ -371,17 +379,21 @@ class _FindShiftsTabState extends State { ), const SizedBox(width: UiConstants.space2), Expanded( - child: TextField( - onChanged: (v) => - setState(() => _searchQuery = v), - decoration: InputDecoration( - border: InputBorder.none, - hintText: context - .t - .staff_shifts - .find_shifts - .search_hint, - hintStyle: UiTypography.body2r.textPlaceholder, + child: Semantics( + identifier: 'find_shifts_search_input', + child: TextField( + controller: _searchController, + onChanged: (v) => + setState(() => _searchQuery = v), + decoration: InputDecoration( + border: InputBorder.none, + hintText: context + .t + .staff_shifts + .find_shifts + .search_hint, + hintStyle: UiTypography.body2r.textPlaceholder, + ), ), ), ), diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/widgets/staff_main_bottom_bar.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/widgets/staff_main_bottom_bar.dart index 176719ed..c4ba20ed 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/widgets/staff_main_bottom_bar.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/widgets/staff_main_bottom_bar.dart @@ -123,26 +123,30 @@ class StaffMainBottomBar extends StatelessWidget { final bool isSelected = currentIndex == item.index; return Expanded( - child: GestureDetector( - onTap: () => onTap(item.index), - behavior: HitTestBehavior.opaque, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Icon( - item.icon, - color: isSelected ? activeColor : inactiveColor, - size: UiConstants.iconLg, - ), - const SizedBox(height: UiConstants.space1), - Text( - item.label, - style: UiTypography.footnote2m.copyWith( + child: Semantics( + identifier: 'nav_${item.tabKey}', + label: item.label, + child: GestureDetector( + onTap: () => onTap(item.index), + behavior: HitTestBehavior.opaque, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + item.icon, color: isSelected ? activeColor : inactiveColor, + size: UiConstants.iconLg, ), - ), - ], + const SizedBox(height: UiConstants.space1), + Text( + item.label, + style: UiTypography.footnote2m.copyWith( + color: isSelected ? activeColor : inactiveColor, + ), + ), + ], + ), ), ), ); diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index c08e4dd6..7762f0d4 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -1133,6 +1133,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + pinput: + dependency: transitive + description: + name: pinput + sha256: c41f42ee301505ae2375ec32871c985d3717bf8aee845620465b286e0140aad2 + url: "https://pub.dev" + source: hosted + version: "5.0.2" platform: dependency: transitive description: @@ -1434,6 +1442,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.12.0" + smart_auth: + dependency: transitive + description: + name: smart_auth + sha256: a25229b38c02f733d0a4e98d941b42bed91a976cb589e934895e60ccfa674cf6 + url: "https://pub.dev" + source: hosted + version: "1.1.1" source_map_stack_trace: dependency: transitive description: @@ -1544,6 +1560,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" url_launcher: dependency: transitive description: