Initial commit

This commit is contained in:
Mike Kell 2025-12-25 18:22:46 -05:00
commit 96dcfb11f4
164 changed files with 11839 additions and 0 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}

55
README.md Normal file
View File

@ -0,0 +1,55 @@
# WooCommerce Inventory Manager
A modern web application to manage your WooCommerce products, inventory, and plugin metadata.
## Prerequisites
- Python 3.8+
- Node.js 16+
- WooCommerce Store with REST API enabled (Consumer Key & Secret required)
## Setup & Run
### 1. Start the Backend
The backend runs on Python/FastAPI.
```bash
# Open a new terminal
pip install -r backend/requirements.txt
cd backend
python main.py
```
Server will start at `http://0.0.0.0:8000`.
### 2. Start the Frontend (Flutter)
The frontend is a unified Flutter app for Web, Android, and Windows.
```bash
# Open a new terminal
cd mobile_app
flutter pub get
flutter run -d chrome # For Web
# OR
flutter run -d windows # For PC
# OR
flutter run -d android # For Mobile
```
> **Note**: Logic for connecting to the backend varies by platform:
> * **Web/Windows**: Use `localhost:8000`
> * **Android Emulator**: Use `10.0.2.2:8000`
> * **Physical Device**: Use your computer's local IP (e.g., `192.168.1.X:8000`)
## 📂 Legacy Frontend
The old React frontend has been moved to `legacy_frontend/` and is deprecated.
## Features
- **Dashboard**: View all products, search, and manage stock.
- **Product Editor**:
- Edit standard fields (Price, SKU, Stock).
- **Images**: Drag & Drop image upload.
- **Meta Data**: View and edit hidden metadata used by plugins (e.g., Yoast, Bundles).
- **Authentication**: Secure login using your WooCommerce Consumer Key & Secret.
## Troubleshooting
- If you see "CORS" errors, ensure the backend is running.
- If images fail to upload, check your WordPress media permissions.

260
backend/main.py Normal file
View File

@ -0,0 +1,260 @@
from fastapi import FastAPI, HTTPException, Body, File, UploadFile, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import requests
from requests.auth import HTTPBasicAuth
import os
from typing import List, Optional, Dict, Any
from dotenv import load_dotenv
load_dotenv()
import logging
# Configure logging to file
logging.basicConfig(
filename='debug.log',
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = FastAPI()
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, replace with specific origin
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def log_requests(request: Request, call_next):
logger.info(f"Incoming request: {request.method} {request.url}")
try:
response = await call_next(request)
logger.info(f"Response status: {response.status_code}")
return response
except Exception as e:
logger.error(f"Request failed: {str(e)}")
raise e
class LoginRequest(BaseModel):
url: str
consumer_key: str
consumer_secret: str
class Product(BaseModel):
name: str
type: str = "simple"
regular_price: str = ""
description: str = ""
short_description: str = ""
categories: List[Dict[str, Any]] = []
images: List[Dict[str, Any]] = []
meta_data: List[Dict[str, Any]] = []
manage_stock: bool = False
stock_quantity: Optional[int] = None
stock_status: str = "instock"
@app.get("/")
def read_root():
return {"message": "WooCommerce Inventory API is running"}
@app.post("/login")
def login(request: LoginRequest):
logger.info(f"Login attempt for URL: {request.url}")
# Verify credentials by making a lightweight call to the API
try:
# Ensure URL has protocol
base_url = request.url.strip()
if not base_url.startswith('http'):
base_url = f'https://{base_url}'
url = f"{base_url.rstrip('/')}/wp-json/wc/v3/system_status"
logger.debug(f"Connecting to: {url}")
# Add User-Agent to avoid blocking by security plugins
headers = {
'User-Agent': 'WooCommerce-Inventory-App/1.0'
}
response = requests.get(
url,
auth=HTTPBasicAuth(request.consumer_key, request.consumer_secret),
headers=headers
)
logger.info(f"WooCommerce API Response: {response.status_code}")
if response.status_code == 200:
return {"status": "success", "message": "Login successful"}
else:
logger.warning(f"Login failed. Status: {response.status_code}, Body: {response.text}")
# Try to return a helpful error message
try:
error_detail = response.json().get('message', response.text)
except:
error_detail = response.text
raise HTTPException(status_code=401, detail=f"WooCommerce Error: {error_detail}")
except HTTPException as he:
raise he
except Exception as e:
logger.error(f"Login exception: {str(e)}")
raise HTTPException(status_code=400, detail=f"Connection failed: {str(e)}")
@app.get("/products")
def get_products(
url: str,
consumer_key: str,
consumer_secret: str,
page: int = 1,
per_page: int = 20,
search: Optional[str] = None
):
try:
api_url = f"{url.rstrip('/')}/wp-json/wc/v3/products"
params = {
"page": page,
"per_page": per_page
}
if search:
params["search"] = search
response = requests.get(
api_url,
auth=HTTPBasicAuth(consumer_key, consumer_secret),
params=params
)
if response.status_code == 200:
return response.json()
else:
raise HTTPException(status_code=response.status_code, detail=response.text)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/products/{product_id}")
def get_product(
product_id: int,
url: str,
consumer_key: str,
consumer_secret: str
):
try:
api_url = f"{url.rstrip('/')}/wp-json/wc/v3/products/{product_id}"
response = requests.get(
api_url,
auth=HTTPBasicAuth(consumer_key, consumer_secret)
)
if response.status_code == 200:
return response.json()
else:
raise HTTPException(status_code=response.status_code, detail=response.text)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/products")
def create_product(
url: str,
consumer_key: str,
consumer_secret: str,
product: Dict[str, Any] = Body(...)
):
try:
api_url = f"{url.rstrip('/')}/wp-json/wc/v3/products"
response = requests.post(
api_url,
auth=HTTPBasicAuth(consumer_key, consumer_secret),
json=product
)
if response.status_code == 201:
return response.json()
else:
raise HTTPException(status_code=response.status_code, detail=response.text)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.put("/products/{product_id}")
def update_product(
product_id: int,
url: str,
consumer_key: str,
consumer_secret: str,
product: Dict[str, Any] = Body(...)
):
try:
api_url = f"{url.rstrip('/')}/wp-json/wc/v3/products/{product_id}"
response = requests.put(
api_url,
auth=HTTPBasicAuth(consumer_key, consumer_secret),
json=product
)
if response.status_code == 200:
return response.json()
else:
raise HTTPException(status_code=response.status_code, detail=response.text)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/products/{product_id}")
def delete_product(
product_id: int,
url: str,
consumer_key: str,
consumer_secret: str,
force: bool = True
):
try:
api_url = f"{url.rstrip('/')}/wp-json/wc/v3/products/{product_id}"
params = {"force": str(force).lower()}
response = requests.delete(
api_url,
auth=HTTPBasicAuth(consumer_key, consumer_secret),
params=params
)
if response.status_code == 200:
return response.json()
else:
raise HTTPException(status_code=response.status_code, detail=response.text)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/upload")
async def upload_image(
url: str,
consumer_key: str,
consumer_secret: str,
file: UploadFile = File(...)
):
# Uploading images to WooCommerce via API is a bit different, typically POST to /wp-json/wp/v2/media
try:
api_url = f"{url.rstrip('/')}/wp-json/wp/v2/media"
# Read file content
content = await file.read()
headers = {
"Content-Disposition": f"attachment; filename={file.filename}",
"Content-Type": file.content_type
}
response = requests.post(
api_url,
auth=HTTPBasicAuth(consumer_key, consumer_secret),
data=content,
headers=headers
)
if response.status_code == 201:
return response.json()
else:
raise HTTPException(status_code=response.status_code, detail=response.text)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

5
backend/requirements.txt Normal file
View File

@ -0,0 +1,5 @@
fastapi
uvicorn
requests
python-multipart
python-dotenv

48
connect_inventory.py Normal file
View File

@ -0,0 +1,48 @@
import requests
from requests.auth import HTTPBasicAuth
# Configuration
CK = 'ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317'
CS = 'cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625'
BASE_URL = 'https://kellcreations.com/wp-json/wc/v3'
def get_products():
endpoint = f'{BASE_URL}/products'
# Try fetching up to 20 products
params = {
'per_page': 20
}
print(f"Connecting to {endpoint}...")
try:
response = requests.get(
endpoint,
auth=HTTPBasicAuth(CK, CS),
params=params
)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
products = response.json()
print(f"Found {len(products)} products:")
print("-" * 50)
for product in products:
name = product.get('name', 'N/A')
stock_status = product.get('stock_status', 'N/A')
stock_quantity = product.get('stock_quantity', 'N/A')
price = product.get('price', 'N/A')
print(f"Name: {name}")
print(f" Price: {price}")
print(f" Stock: {stock_status} ({stock_quantity})")
print("-" * 50)
else:
print("Failed to retrieve products.")
print(f"Response: {response.text}")
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
get_products()

112
debug.log Normal file
View File

@ -0,0 +1,112 @@
2025-12-16 06:12:49,426 - asyncio - DEBUG - Using proactor: IocpProactor
2025-12-16 06:13:47,570 - __main__ - INFO - Incoming request: POST http://localhost:8000/login
2025-12-16 06:13:47,573 - __main__ - INFO - Login attempt for URL: https://kellcreations.com/wp-json/wc/v3
2025-12-16 06:13:47,573 - __main__ - DEBUG - Connecting to: https://kellcreations.com/wp-json/wc/v3/wp-json/wc/v3/system_status
2025-12-16 06:13:47,575 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:13:51,944 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/wp-json/wc/v3/system_status HTTP/1.1" 404 117
2025-12-16 06:13:51,945 - __main__ - INFO - WooCommerce API Response: 404
2025-12-16 06:13:51,945 - __main__ - WARNING - Login failed. Status: 404, Body: {"code":"rest_no_route","message":"No route was found matching the URL and request method.","data":{"status":404}}
2025-12-16 06:13:51,945 - __main__ - ERROR - Login exception: 401: Invalid credentials or URL
2025-12-16 06:13:51,946 - __main__ - INFO - Response status: 400
2025-12-16 06:16:16,073 - asyncio - DEBUG - Using proactor: IocpProactor
2025-12-16 06:16:26,288 - __main__ - INFO - Incoming request: OPTIONS http://localhost:8000/login
2025-12-16 06:16:26,288 - __main__ - INFO - Response status: 200
2025-12-16 06:16:26,291 - __main__ - INFO - Incoming request: POST http://localhost:8000/login
2025-12-16 06:16:26,294 - __main__ - INFO - Login attempt for URL: https://kellcreations.com/wp-json/wc/v3
2025-12-16 06:16:26,294 - __main__ - DEBUG - Connecting to: https://kellcreations.com/wp-json/wc/v3/wp-json/wc/v3/system_status
2025-12-16 06:16:26,295 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:16:29,485 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/wp-json/wc/v3/system_status HTTP/1.1" 404 117
2025-12-16 06:16:29,485 - __main__ - INFO - WooCommerce API Response: 404
2025-12-16 06:16:29,486 - __main__ - WARNING - Login failed. Status: 404, Body: {"code":"rest_no_route","message":"No route was found matching the URL and request method.","data":{"status":404}}
2025-12-16 06:16:29,486 - __main__ - INFO - Response status: 401
2025-12-16 06:17:13,776 - __main__ - INFO - Incoming request: POST http://localhost:8000/login
2025-12-16 06:17:13,777 - __main__ - INFO - Login attempt for URL: https://kellcreations.com
2025-12-16 06:17:13,777 - __main__ - DEBUG - Connecting to: https://kellcreations.com/wp-json/wc/v3/system_status
2025-12-16 06:17:13,778 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:17:17,649 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/system_status HTTP/1.1" 200 6831
2025-12-16 06:17:17,650 - __main__ - INFO - WooCommerce API Response: 200
2025-12-16 06:17:17,651 - __main__ - INFO - Response status: 200
2025-12-16 06:17:17,712 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
2025-12-16 06:17:17,739 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:17:23,369 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
2025-12-16 06:17:23,483 - __main__ - INFO - Response status: 200
2025-12-16 06:17:23,487 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
2025-12-16 06:17:23,488 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:17:28,122 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
2025-12-16 06:17:29,200 - __main__ - INFO - Response status: 200
2025-12-16 06:19:42,438 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/250214?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
2025-12-16 06:19:42,589 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:19:46,914 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/250214 HTTP/1.1" 200 6617
2025-12-16 06:19:46,920 - __main__ - INFO - Response status: 200
2025-12-16 06:19:46,923 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/250214?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
2025-12-16 06:19:46,924 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:19:50,315 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/250214 HTTP/1.1" 200 6618
2025-12-16 06:19:50,321 - __main__ - INFO - Response status: 200
2025-12-16 06:23:13,565 - __main__ - INFO - Incoming request: POST http://localhost:8000/login
2025-12-16 06:23:13,566 - __main__ - INFO - Login attempt for URL: https://kellcreations.com
2025-12-16 06:23:13,566 - __main__ - DEBUG - Connecting to: https://kellcreations.com/wp-json/wc/v3/system_status
2025-12-16 06:23:13,567 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:23:16,821 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/system_status HTTP/1.1" 200 6831
2025-12-16 06:23:16,821 - __main__ - INFO - WooCommerce API Response: 200
2025-12-16 06:23:16,823 - __main__ - INFO - Response status: 200
2025-12-16 06:24:09,346 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
2025-12-16 06:24:09,348 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:24:14,129 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
2025-12-16 06:24:14,250 - __main__ - INFO - Response status: 200
2025-12-16 06:24:14,254 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
2025-12-16 06:24:14,256 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:24:19,343 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
2025-12-16 06:24:19,460 - __main__ - INFO - Response status: 200
2025-12-16 06:24:58,327 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/250214?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
2025-12-16 06:24:58,329 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:25:03,169 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/250214 HTTP/1.1" 200 6618
2025-12-16 06:25:03,175 - __main__ - INFO - Response status: 200
2025-12-16 06:25:03,178 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/250214?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
2025-12-16 06:25:03,179 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:25:06,907 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/250214 HTTP/1.1" 200 6618
2025-12-16 06:25:06,913 - __main__ - INFO - Response status: 200
2025-12-16 06:26:31,062 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
2025-12-16 06:26:31,063 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:26:35,770 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
2025-12-16 06:26:36,868 - __main__ - INFO - Response status: 200
2025-12-16 06:26:36,871 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
2025-12-16 06:26:36,872 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:26:40,470 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/250076?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
2025-12-16 06:26:40,472 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:26:41,600 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
2025-12-16 06:26:42,669 - __main__ - INFO - Response status: 200
2025-12-16 06:26:44,474 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/250076 HTTP/1.1" 200 4894
2025-12-16 06:26:44,478 - __main__ - INFO - Response status: 200
2025-12-16 06:26:44,480 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/250076?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
2025-12-16 06:26:44,482 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:26:47,481 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/250076 HTTP/1.1" 200 4897
2025-12-16 06:26:47,484 - __main__ - INFO - Response status: 200
2025-12-16 06:27:25,702 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
2025-12-16 06:27:26,196 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:27:31,008 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
2025-12-16 06:27:32,079 - __main__ - INFO - Response status: 200
2025-12-16 06:27:32,081 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
2025-12-16 06:27:32,083 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:27:36,676 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
2025-12-16 06:27:37,770 - __main__ - INFO - Response status: 200
2025-12-16 06:27:42,047 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=2&search=
2025-12-16 06:27:42,048 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:27:47,834 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=2&per_page=20 HTTP/1.1" 200 39630
2025-12-16 06:27:47,888 - __main__ - INFO - Response status: 200
2025-12-16 06:28:10,795 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/249378?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
2025-12-16 06:28:10,796 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:28:14,227 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/249378 HTTP/1.1" 200 6421
2025-12-16 06:28:14,233 - __main__ - INFO - Response status: 200
2025-12-16 06:28:14,235 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/249378?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
2025-12-16 06:28:14,237 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:28:17,396 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/249378 HTTP/1.1" 200 6421
2025-12-16 06:28:17,402 - __main__ - INFO - Response status: 200
2025-12-16 06:29:22,094 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
2025-12-16 06:29:22,095 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:29:26,351 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
2025-12-16 06:29:27,421 - __main__ - INFO - Response status: 200
2025-12-16 06:29:27,423 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
2025-12-16 06:29:27,425 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
2025-12-16 06:29:32,212 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
2025-12-16 06:29:33,283 - __main__ - INFO - Response status: 200
2025-12-25 18:12:03,135 - asyncio - DEBUG - Using proactor: IocpProactor

24
legacy_frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

16
legacy_frontend/README.md Normal file
View File

@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

View File

@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3889
legacy_frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.18",
"axios": "^1.13.2",
"lucide-react": "^0.561.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.10.1",
"react-toastify": "^11.0.5"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"vite": "^7.2.4"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import Login from './components/Login';
import ProductList from './components/ProductList';
import ProductEditor from './components/ProductEditor';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Login />} />
<Route path="/dashboard" element={<ProductList />} />
<Route path="/product/:id" element={<ProductEditor />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
);
}
export default App;

View File

@ -0,0 +1,97 @@
import axios from 'axios';
const API_URL = 'http://localhost:8000'; // Adjust if backend runs elsewhere
export const api = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
});
export const login = async (url, consumer_key, consumer_secret) => {
const response = await api.post('/login', { url, consumer_key, consumer_secret });
// Store creds in localStorage for convenience in this demo app
localStorage.setItem('wc_creds', JSON.stringify({ url, consumer_key, consumer_secret }));
return response.data;
};
export const getStoredCreds = () => {
const stored = localStorage.getItem('wc_creds');
return stored ? JSON.parse(stored) : null;
};
export const logout = () => {
localStorage.removeItem('wc_creds');
};
export const getProducts = async (page = 1, search = '') => {
const creds = getStoredCreds();
if (!creds) throw new Error('Not logged in');
const response = await api.get('/products', {
params: {
...creds,
page,
search
}
});
return response.data;
};
export const getProduct = async (id) => {
const creds = getStoredCreds();
if (!creds) throw new Error('Not logged in');
const response = await api.get(`/products/${id}`, {
params: { ...creds }
});
return response.data;
};
export const createProduct = async (productData) => {
const creds = getStoredCreds();
if (!creds) throw new Error('Not logged in');
const response = await api.post('/products', productData, {
params: { ...creds }
});
return response.data;
};
export const updateProduct = async (id, productData) => {
const creds = getStoredCreds();
if (!creds) throw new Error('Not logged in');
const response = await api.put(`/products/${id}`, productData, {
params: { ...creds }
});
return response.data;
};
export const deleteProduct = async (id) => {
const creds = getStoredCreds();
if (!creds) throw new Error('Not logged in');
const response = await api.delete(`/products/${id}`, {
params: { ...creds }
});
return response.data;
};
export const uploadImage = async (file) => {
const creds = getStoredCreds();
if (!creds) throw new Error('Not logged in');
const formData = new FormData();
formData.append('file', file);
// Pass creds as query params because we are sending form data body
const response = await api.post('/upload', formData, {
params: { ...creds },
headers: {
'Content-Type': 'multipart/form-data'
}
});
return response.data;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,114 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { login } from '../api';
import { Key, Lock, Globe } from 'lucide-react';
export default function Login() {
const [url, setUrl] = useState('');
const [ck, setCk] = useState('');
const [cs, setCs] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
console.log('Login button clicked');
console.log('Credentials:', { url, ck, cs });
setLoading(true);
setError('');
try {
console.log('Calling login API...');
const response = await login(url, ck, cs);
console.log('Login successful:', response);
navigate('/dashboard');
} catch (err) {
console.error('Login error:', err);
setError(err.response?.data?.detail || 'Login failed. Please check your credentials.');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900 px-4">
<div className="max-w-md w-full bg-gray-800 rounded-lg shadow-xl p-8 border border-gray-700">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-white mb-2">WooCommerce Manager</h1>
<p className="text-gray-400">Enter your store details to connect</p>
</div>
{error && (
<div className="bg-red-900/50 border border-red-500 text-red-200 p-3 rounded mb-6 text-sm">
{error}
</div>
)}
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Store URL</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Globe className="h-5 w-5 text-gray-500" />
</div>
<input
type="url"
required
className="block w-full pl-10 bg-gray-700 border-gray-600 rounded-md py-2.5 text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
placeholder="https://example.com"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Consumer Key</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Key className="h-5 w-5 text-gray-500" />
</div>
<input
type="text"
required
className="block w-full pl-10 bg-gray-700 border-gray-600 rounded-md py-2.5 text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
placeholder="ck_..."
value={ck}
onChange={(e) => setCk(e.target.value)}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Consumer Secret</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-500" />
</div>
<input
type="password"
required
className="block w-full pl-10 bg-gray-700 border-gray-600 rounded-md py-2.5 text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
placeholder="cs_..."
value={cs}
onChange={(e) => setCs(e.target.value)}
/>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full flex justify-center py-4 px-6 border border-transparent rounded-lg shadow-lg text-lg font-bold text-white uppercase tracking-wider bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 hover:shadow-xl hover:scale-[1.02] transform transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
>
{loading ? 'Connecting...' : 'CONNECT TO STORE'}
</button>
<div className="text-xs text-gray-500 text-center mt-4">
Ensure REST API is enabled in your WooCommerce settings.
</div>
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,391 @@
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { getProduct, createProduct, updateProduct, uploadImage } from '../api';
import { ArrowLeft, Save, Plus, X, Upload, Image as ImageIcon, Trash2 } from 'lucide-react';
export default function ProductEditor() {
const { id } = useParams();
const isNew = id === 'new';
const navigate = useNavigate();
const [loading, setLoading] = useState(!isNew); // If new, not loading
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
// Product State
const [product, setProduct] = useState({
name: '',
type: 'simple',
regular_price: '',
description: '',
short_description: '',
sku: '',
manage_stock: false,
stock_quantity: 0,
stock_status: 'instock',
images: [],
meta_data: []
});
useEffect(() => {
if (!isNew) {
fetchProduct();
}
}, [id]);
const fetchProduct = async () => {
try {
const data = await getProduct(id);
setProduct(data);
} catch (err) {
setError('Failed to load product');
} finally {
setLoading(false);
}
};
const handleChange = (field, value) => {
setProduct(prev => ({ ...prev, [field]: value }));
};
const handleMetaChange = (index, key, value) => {
const newMeta = [...product.meta_data];
newMeta[index] = { ...newMeta[index], key, value };
setProduct(prev => ({ ...prev, meta_data: newMeta }));
};
const addMeta = () => {
setProduct(prev => ({
...prev,
meta_data: [...prev.meta_data, { key: '', value: '' }]
}));
};
const removeMeta = (index) => {
setProduct(prev => ({
...prev,
meta_data: prev.meta_data.filter((_, i) => i !== index)
}));
};
const handleImageUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
setSaving(true); // Reuse saving state for upload indicator
try {
const media = await uploadImage(file);
setProduct(prev => ({
...prev,
images: [...prev.images, { id: media.id, src: media.source_url }]
}));
} catch (err) {
alert('Failed to upload image');
} finally {
setSaving(false);
}
};
const removeImage = (index) => {
setProduct(prev => ({
...prev,
images: prev.images.filter((_, i) => i !== index)
}));
};
const handleSave = async (e) => {
e.preventDefault();
setSaving(true);
setError('');
try {
if (isNew) {
await createProduct(product);
} else {
await updateProduct(id, product);
}
navigate('/dashboard');
} catch (err) {
setError(err.response?.data?.detail || 'Failed to save product');
} finally {
setSaving(false);
}
};
if (loading) return <div className="text-center text-white mt-10">Loading...</div>;
return (
<div className="min-h-screen bg-gray-900 text-gray-100 p-8">
<div className="max-w-4xl mx-auto">
<div className="flex items-center justify-between mb-8">
<button onClick={() => navigate('/dashboard')} className="flex items-center text-gray-400 hover:text-white transition">
<ArrowLeft className="h-5 w-5 mr-1" />
Back to Dashboard
</button>
<h1 className="text-2xl font-bold">{isNew ? 'Create New Product' : 'Edit Product'}</h1>
<button
onClick={handleSave}
disabled={saving}
className="flex items-center bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition disabled:opacity-50"
>
<Save className="h-5 w-5 mr-2" />
{saving ? 'Saving...' : 'Save Product'}
</button>
</div>
{error && (
<div className="bg-red-900/50 border border-red-500 text-red-200 p-4 rounded-lg mb-6">
{error}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Main Info */}
<div className="md:col-span-2 space-y-6">
<div className="bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-lg">
<h2 className="text-lg font-semibold mb-4 text-blue-400">General Information</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Product Name</label>
<input
type="text"
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
value={product.name}
onChange={(e) => handleChange('name', e.target.value)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Regular Price</label>
<input
type="text"
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
value={product.regular_price}
onChange={(e) => handleChange('regular_price', e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">SKU</label>
<input
type="text"
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
value={product.sku || ''}
onChange={(e) => handleChange('sku', e.target.value)}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Short Description</label>
<textarea
rows="3"
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
value={product.short_description}
onChange={(e) => handleChange('short_description', e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Full Description</label>
<textarea
rows="5"
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
value={product.description}
onChange={(e) => handleChange('description', e.target.value)}
/>
</div>
</div>
</div>
{/* Inventory */}
<div className="bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-lg">
<h2 className="text-lg font-semibold mb-4 text-blue-400">Inventory</h2>
<div className="space-y-4">
<div className="flex items-center">
<input
type="checkbox"
id="manage_stock"
checked={product.manage_stock}
onChange={(e) => handleChange('manage_stock', e.target.checked)}
className="h-4 w-4 bg-gray-700 border-gray-600 rounded text-blue-600 focus:ring-blue-500"
/>
<label htmlFor="manage_stock" className="ml-2 text-sm font-medium text-gray-300">Enable Stock Management</label>
</div>
{product.manage_stock && (
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Stock Quantity</label>
<input
type="number"
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
value={product.stock_quantity || 0}
onChange={(e) => handleChange('stock_quantity', parseInt(e.target.value))}
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Stock Status</label>
<select
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
value={product.stock_status}
onChange={(e) => handleChange('stock_status', e.target.value)}
>
<option value="instock">In Stock</option>
<option value="outofstock">Out of Stock</option>
<option value="onbackorder">On Backorder</option>
</select>
</div>
</div>
</div>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Images */}
<div className="bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-lg">
<h2 className="text-lg font-semibold mb-4 text-blue-400">Product Images</h2>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-2">
{product.images.map((img, idx) => (
<div key={idx} className="relative group">
<img src={img.src} alt="" className="w-full h-24 object-cover rounded-md border border-gray-600" />
<button
onClick={() => removeImage(idx)}
className="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition"
>
<X className="h-3 w-3" />
</button>
</div>
))}
</div>
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-gray-600 border-dashed rounded-lg cursor-pointer hover:bg-gray-750 transition">
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<Upload className="w-8 h-8 mb-2 text-gray-400" />
<p className="text-xs text-gray-500">Click to upload image</p>
</div>
<input type="file" className="hidden" accept="image/*" onChange={handleImageUpload} />
</label>
</div>
</div>
{/* Meta Data / Plugins */}
{/* Meta Data / Plugins (Grouped) */}
<div className="bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-lg">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold text-blue-400">Plugin Settings</h2>
<button onClick={addMeta} className="text-blue-400 hover:text-blue-300 text-sm flex items-center">
<Plus className="h-4 w-4 mr-1" />
Add Custom Meta
</button>
</div>
<div className="space-y-6 max-h-[600px] overflow-y-auto pr-2 custom-scrollbar">
{(() => {
// Grouping Logic
const groups = {
'Yoast SEO': { prefix: '_yoast_', items: [], icon: '🔍' },
'Product Bundles': { prefix: '_bundle', items: [], icon: '📦' },
'Facebook': { prefix: 'fb_', items: [], icon: 'f' },
'Google Listings': { prefix: '_wc_gla', items: [], icon: 'G' },
'Tax Settings': { prefix: '_tax', items: [], icon: '💰' },
'Other': { prefix: '', items: [], icon: '🔧' }
};
// Helper to find readable name
const getReadableName = (key) => {
const names = {
'_yoast_wpseo_title': 'SEO Title',
'_yoast_wpseo_metadesc': 'Meta Description',
'_yoast_wpseo_focuskw': 'Focus Keyword',
'_regular_price': 'Regular Price (Meta)',
'_price': 'Price (Meta)',
'_sku': 'SKU (Meta)',
'_tax_status': 'Tax Status',
'_tax_class': 'Tax Class',
'_bundle_data': 'Bundle Configuration',
'_bundled_items': 'Bundled Item IDs'
};
return names[key] || key;
};
// Sort items into groups
product.meta_data.forEach((meta, idx) => {
if (meta.key.startsWith('_')) {
// System/Hidden keys often start with _
}
let placed = false;
for (const [groupName, config] of Object.entries(groups)) {
if (groupName !== 'Other' && meta.key.includes(config.prefix)) {
groups[groupName].items.push({ ...meta, originalIndex: idx });
placed = true;
break;
}
}
if (!placed) {
groups['Other'].items.push({ ...meta, originalIndex: idx });
}
});
// Render Groups
return Object.entries(groups).map(([groupName, config]) => {
if (config.items.length === 0) return null;
return (
<div key={groupName} className="bg-gray-750/50 rounded-lg p-3 border border-gray-600/50">
<h3 className="text-sm font-bold text-gray-300 mb-3 flex items-center uppercase tracking-wider">
<span className="mr-2 opacity-75">{config.icon}</span>
{groupName}
</h3>
<div className="space-y-3">
{config.items.map((meta) => (
<div key={meta.originalIndex} className="bg-gray-900/50 p-3 rounded border border-gray-700 relative group transition hover:border-blue-500/30">
<div className="space-y-1">
<div className="flex justify-between items-center mb-1">
<label className="text-xs font-semibold text-blue-300" title={meta.key}>
{getReadableName(meta.key)}
</label>
<span className="text-[10px] text-gray-600 font-mono hidden group-hover:block">{meta.key}</span>
</div>
{typeof meta.value === 'object' ? (
<div className="text-xs text-gray-500 italic bg-gray-900 p-2 rounded">
Complex Data (Editing disabled for safety)
<details className="mt-1">
<summary className="cursor-pointer hover:text-white">View JSON</summary>
<pre className="mt-1 overflow-x-auto">{JSON.stringify(meta.value, null, 2)}</pre>
</details>
</div>
) : meta.value && meta.value.length > 50 ? (
<textarea
className="w-full bg-gray-800 border-gray-700 rounded px-2 py-1 text-sm text-gray-200 outline-none focus:ring-1 focus:ring-blue-500 min-h-[60px]"
value={meta.value}
onChange={(e) => handleMetaChange(meta.originalIndex, e.target.value, meta.value)} // Corrected handler signature
/>
) : (
<input
type="text"
className="w-full bg-gray-800 border-gray-700 rounded px-2 py-1 text-sm text-gray-200 outline-none focus:ring-1 focus:ring-blue-500"
value={meta.value}
onChange={(e) => handleMetaChange(meta.originalIndex, meta.key, e.target.value)}
/>
)}
</div>
<button
onClick={() => removeMeta(meta.originalIndex)}
className="absolute top-2 right-2 text-gray-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition p-1"
title="Remove Field"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
))}
</div>
</div>
);
});
})()}
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,263 @@
import React, { useEffect, useState } from 'react';
import { getProducts, deleteProduct, logout } from '../api';
import { Link, useNavigate } from 'react-router-dom';
import { Plus, Search, Trash2, Edit, LogOut, Package, Image as ImageIcon, ChevronLeft, ChevronRight } from 'lucide-react';
export default function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const [error, setError] = useState('');
const [viewMode, setViewMode] = useState('table');
const navigate = useNavigate();
const fetchProducts = async () => {
setLoading(true);
try {
const data = await getProducts(page, search);
setProducts(data);
setError('');
} catch (err) {
console.error(err);
if (err.message === 'Not logged in') {
navigate('/');
} else {
setError('Failed to load products.');
}
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchProducts();
}, [page]); // Re-fetch when page changes
// Search effect with debounce could be added, but manual search button for now or just simple effect
const handleSearch = (e) => {
e.preventDefault();
setPage(1);
fetchProducts();
};
const handleDelete = async (id) => {
if (window.confirm('Are you sure you want to delete this product?')) {
try {
await deleteProduct(id);
fetchProducts(); // Refresh
} catch (err) {
alert('Failed to delete product');
}
}
};
const handleLogout = () => {
logout();
navigate('/');
};
return (
<div className="min-h-screen bg-gray-900 text-gray-100">
{/* Navbar */}
<nav className="bg-gray-800 border-b border-gray-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center">
<span className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-teal-400">
Inventory Manager
</span>
</div>
<div className="flex items-center space-x-4">
<div className="bg-gray-700 rounded-lg p-1 flex">
<button
onClick={() => setViewMode('table')}
className={`p-2 rounded-md transition ${viewMode === 'table' ? 'bg-gray-600 text-white shadow-sm' : 'text-gray-400 hover:text-white'}`}
title="Table View"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" /></svg>
</button>
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded-md transition ${viewMode === 'grid' ? 'bg-gray-600 text-white shadow-sm' : 'text-gray-400 hover:text-white'}`}
title="Grid View"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
</button>
</div>
<button onClick={handleLogout} className="flex items-center text-gray-400 hover:text-white transition">
<LogOut className="h-5 w-5 mr-1" />
Logout
</button>
</div>
</div>
</div>
</nav>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header Actions */}
<div className="flex flex-col sm:flex-row justify-between items-center mb-8 gap-4">
<form onSubmit={handleSearch} className="relative w-full sm:w-96">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="h-5 w-5 text-gray-500" />
</div>
<input
type="text"
placeholder="Search products..."
className="block w-full pl-10 bg-gray-800 border-gray-700 rounded-lg py-2 text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 outline-none"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</form>
<Link
to="/product/new"
className="flex items-center bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition"
>
<Plus className="h-5 w-5 mr-1" />
Add Product
</Link>
</div>
{/* Error */}
{error && (
<div className="bg-red-900/50 border border-red-500 text-red-200 p-4 rounded-lg mb-6">
{error}
</div>
)}
{loading ? (
<div className="text-center py-20 text-gray-400">Loading products...</div>
) : products.length === 0 ? (
<div className="text-center py-20 text-gray-400">No products found.</div>
) : (
<>
{viewMode === 'table' ? (
<div className="bg-gray-800 rounded-xl shadow-xl overflow-hidden border border-gray-700">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-700">
<thead className="bg-gray-750">
<tr>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Product</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Price</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Stock</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Status</th>
<th className="px-6 py-4 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700">
{products.map((product) => (
<tr key={product.id} className="hover:bg-gray-750 transition">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="h-16 w-16 flex-shrink-0 bg-gray-700 rounded-md overflow-hidden flex items-center justify-center border border-gray-600">
{product.images?.[0]?.src ? (
<img className="h-full w-full object-cover" src={product.images[0].src} alt="" />
) : (
<ImageIcon className="h-6 w-6 text-gray-500" />
)}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-white max-w-xs truncate" title={product.name}>{product.name}</div>
<div className="text-sm text-gray-500">ID: {product.id}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-300">
<span dangerouslySetInnerHTML={{ __html: product.price_html || ('$' + product.price) }} />
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-300">
{product.manage_stock ? product.stock_quantity : 'Unlimited'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${product.stock_status === 'instock' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{product.stock_status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<Link to={`/product/${product.id}`} className="text-blue-400 hover:text-blue-300 mr-4">
<Edit className="h-5 w-5 inline" />
</Link>
<button onClick={() => handleDelete(product.id)} className="text-red-400 hover:text-red-300">
<Trash2 className="h-5 w-5 inline" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{products.map((product) => (
<div key={product.id} className="bg-gray-800 rounded-xl shadow-lg border border-gray-700 overflow-hidden hover:shadow-xl transition flex flex-col">
<div className="h-48 bg-gray-700 overflow-hidden relative">
{product.images?.[0]?.src ? (
<img className="w-full h-full object-cover" src={product.images[0].src} alt="" />
) : (
<div className="w-full h-full flex items-center justify-center text-gray-500">
<ImageIcon className="h-10 w-10" />
</div>
)}
<div className="absolute top-2 right-2 flex space-x-1">
<span className={`px-2 py-1 text-xs font-semibold rounded-full shadow-sm ${product.stock_status === 'instock' ? 'bg-green-100/90 text-green-800' : 'bg-red-100/90 text-red-800'
}`}>
{product.stock_status === 'instock' ? 'In Stock' : 'Out'}
</span>
</div>
</div>
<div className="p-4 flex-1 flex flex-col">
<div className="flex justify-between items-start mb-2">
<h3 className="text-lg font-bold text-white line-clamp-2" title={product.name}>{product.name}</h3>
</div>
<p className="text-sm text-gray-400 mb-4">ID: {product.id}</p>
<div className="mt-auto flex items-center justify-between">
<span className="text-lg font-semibold text-blue-300" dangerouslySetInnerHTML={{ __html: product.price_html || ('$' + product.price) }} />
<div className="flex space-x-3">
<Link to={`/product/${product.id}`} className="text-gray-400 hover:text-blue-400 transition">
<Edit className="h-5 w-5" />
</Link>
<button onClick={() => handleDelete(product.id)} className="text-gray-400 hover:text-red-400 transition">
<Trash2 className="h-5 w-5" />
</button>
</div>
</div>
</div>
</div>
))}
</div>
)}
</>
)}
{/* Pagination */}
<div className="flex items-center justify-between mt-6">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="flex items-center text-gray-400 hover:text-white disabled:opacity-50"
>
<ChevronLeft className="h-5 w-5 mr-1" />
Previous
</button>
<span className="text-gray-400">Page {page}</span>
<button
onClick={() => setPage(p => p + 1)}
className="flex items-center text-gray-400 hover:text-white"
>
Next
<ChevronRight className="h-5 w-5 ml-1" />
</button>
</div>
</main>
</div>
);
}

View File

@ -0,0 +1,6 @@
@import "tailwindcss";
body {
background-color: #f3f4f6;
font-family: 'Inter', sans-serif;
}

View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

45
mobile_app/.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
mobile_app/.metadata Normal file
View File

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "edada7c56edf4a183c1735310e123c7f923584f1"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
- platform: android
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
- platform: ios
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
- platform: linux
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
- platform: macos
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
- platform: web
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
- platform: windows
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
mobile_app/README.md Normal file
View File

@ -0,0 +1,16 @@
# mobile_app
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
mobile_app/android/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}

View File

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.mobile_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.mobile_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="mobile_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.example.mobile_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,23 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
if (project.name == "app") {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,18 @@
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by net.rubygrapefruit.platform.internal.NativeLibraryLoader in an unnamed module (file:/C:/Users/mtkel/.gradle/wrapper/dists/gradle-8.12-all/ejduaidbjup3bmmkhw3rie4zb/gradle-8.12/lib/native-platform-0.22-milestone-27.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
FAILURE: Build failed with an exception.
* What went wrong:
25
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.
BUILD FAILED in 2s

View File

@ -0,0 +1,43 @@
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by net.rubygrapefruit.platform.internal.NativeLibraryLoader in an unnamed module (file:/C:/Users/mtkel/.gradle/wrapper/dists/gradle-8.12-all/ejduaidbjup3bmmkhw3rie4zb/gradle-8.12/lib/native-platform-0.22-milestone-27.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
Initialized native services in: C:\Users\mtkel\.gradle\native
Initialized jansi services in: C:\Users\mtkel\.gradle\native
Found daemon DaemonInfo{pid=13400, address=[42e99540-8bfb-4471-8279-bf73acaee118 port:59185, addresses:[/127.0.0.1]], state=Idle, lastBusy=1766679695414, context=DefaultDaemonContext[uid=6bf98724-ee1b-4562-b32f-3fa203b52ac2,javaHome=C:\Users\mtkel\.antigravity\extensions\redhat.java-1.50.0-win32-x64\jre\21.0.9-win32-x86_64,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=C:\Users\mtkel\.gradle\daemon,pid=13400,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=4G,-XX:ReservedCodeCacheSize=512m,-XX:+HeapDumpOnOutOfMemoryError,-Xmx8G,-Dfile.encoding=UTF-8,-Duser.country=US,-Duser.language=en,-Duser.variant]} however its context does not match the desired criteria.
JVM is incompatible.
Wanted: DaemonRequestContext{jvmCriteria=C:\Program Files\OpenJDK\jdk-25 (no JDK specified, using current Java home), daemonOpts=[-XX:MaxMetaspaceSize=4G, -XX:ReservedCodeCacheSize=512m, -XX:+HeapDumpOnOutOfMemoryError, -Xmx8G, -Dfile.encoding=UTF-8, -Duser.country=US, -Duser.language=en, -Duser.variant], applyInstrumentationAgent=true, nativeServicesMode=ENABLED, priority=NORMAL}
Actual: DefaultDaemonContext[uid=6bf98724-ee1b-4562-b32f-3fa203b52ac2,javaHome=C:\Users\mtkel\.antigravity\extensions\redhat.java-1.50.0-win32-x64\jre\21.0.9-win32-x86_64,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=C:\Users\mtkel\.gradle\daemon,pid=13400,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=4G,-XX:ReservedCodeCacheSize=512m,-XX:+HeapDumpOnOutOfMemoryError,-Xmx8G,-Dfile.encoding=UTF-8,-Duser.country=US,-Duser.language=en,-Duser.variant]
Looking for a different daemon...
Found daemon DaemonInfo{pid=58204, address=[bbe75fbe-710b-47ab-909d-66a78698b22b port:59247, addresses:[/127.0.0.1]], state=Idle, lastBusy=1766679697302, context=DefaultDaemonContext[uid=a05c09d6-7c75-4e1f-a1df-f745c5df345c,javaHome=C:\Users\mtkel\.antigravity\extensions\redhat.java-1.50.0-win32-x64\jre\21.0.9-win32-x86_64,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=C:\Users\mtkel\.gradle\daemon,pid=58204,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=4G,-XX:ReservedCodeCacheSize=512m,-XX:+HeapDumpOnOutOfMemoryError,-Xmx8G,-Dfile.encoding=UTF-8,-Duser.country=US,-Duser.language=en,-Duser.variant]} however its context does not match the desired criteria.
JVM is incompatible.
Wanted: DaemonRequestContext{jvmCriteria=C:\Program Files\OpenJDK\jdk-25 (no JDK specified, using current Java home), daemonOpts=[-XX:MaxMetaspaceSize=4G, -XX:ReservedCodeCacheSize=512m, -XX:+HeapDumpOnOutOfMemoryError, -Xmx8G, -Dfile.encoding=UTF-8, -Duser.country=US, -Duser.language=en, -Duser.variant], applyInstrumentationAgent=true, nativeServicesMode=ENABLED, priority=NORMAL}
Actual: DefaultDaemonContext[uid=a05c09d6-7c75-4e1f-a1df-f745c5df345c,javaHome=C:\Users\mtkel\.antigravity\extensions\redhat.java-1.50.0-win32-x64\jre\21.0.9-win32-x86_64,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=C:\Users\mtkel\.gradle\daemon,pid=58204,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=4G,-XX:ReservedCodeCacheSize=512m,-XX:+HeapDumpOnOutOfMemoryError,-Xmx8G,-Dfile.encoding=UTF-8,-Duser.country=US,-Duser.language=en,-Duser.variant]
Looking for a different daemon...
The client will now receive all logging from the daemon (pid: 31236). The daemon log file: C:\Users\mtkel\.gradle\daemon\8.12\daemon-31236.out.log
Starting 5th build in daemon [uptime: 1 mins 22.562 secs, performance: 100%, GC rate: 0.00/s, heap usage: 0% of 8 GiB, non-heap usage: 1% of 4 GiB]
Using 6 worker leases.
Now considering [D:\woocommerce_inventory\mobile_app\android] as hierarchies to watch
Watching the file system is configured to be enabled if available
File system watching is active
Starting Build
Invalidating in-memory cache of C:\Users\mtkel\.gradle\caches\journal-1\file-access.bin
Caching disabled for Kotlin DSL script compilation (Settings/TopLevel/stage1) because:
Build cache is disabled
FAILURE: Build failed with an exception.
* What went wrong:
25
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.
BUILD FAILED in 1s
Watched directory hierarchies: []

View File

@ -0,0 +1,5 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.java.home=C:\\Program Files\\Microsoft\\jdk-17.0.17.10-hotspot
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

View File

@ -0,0 +1,25 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

34
mobile_app/ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Mobile App</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>mobile_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@ -0,0 +1,168 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
class ApiService with ChangeNotifier {
String? _baseUrl;
String? _consumerKey;
String? _consumerSecret;
bool _isLoading = false;
String? _error;
bool get isLoading => _isLoading;
String? get error => _error;
bool get isLoggedIn => _baseUrl != null && _consumerKey != null && _consumerSecret != null;
ApiService();
void connect(String url, String key, String secret) {
_baseUrl = url;
_consumerKey = key;
_consumerSecret = secret;
notifyListeners();
}
Future<void> verifyCredentials(String url, String key, String secret) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
// Normalize URL
String baseUrl = url.trim();
if (!baseUrl.startsWith('http')) baseUrl = 'https://$baseUrl';
if (baseUrl.endsWith('/')) baseUrl = baseUrl.substring(0, baseUrl.length - 1);
// Construct WC API URL
final apiUri = Uri.parse('$baseUrl/wp-json/wc/v3/system_status');
final response = await http.get(
apiUri,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ${base64Encode(utf8.encode('$key:$secret'))}',
},
);
if (response.statusCode != 200) {
String msg = 'Connection failed: ${response.statusCode}';
try {
final body = jsonDecode(response.body);
msg = body['message'] ?? msg;
} catch (_) {}
throw Exception(msg);
}
} catch (e) {
_error = e.toString().contains('FormatException')
? 'Invalid Response. Is this a WooCommerce site?'
: e.toString().replaceAll('Exception: ', '');
rethrow;
} finally {
_isLoading = false;
notifyListeners();
}
}
void disconnect() {
_baseUrl = null;
_consumerKey = null;
_consumerSecret = null;
notifyListeners();
}
Future<void> logout() async {
disconnect();
}
// --- Products ---
Map<String, String> get _headers => {
'Content-Type': 'application/json',
'Authorization': 'Basic ${base64Encode(utf8.encode('$_consumerKey:$_consumerSecret'))}',
};
// Helper to build WC API URIs
Uri _getUri(String path, [Map<String, String>? queryParams]) {
// Ensuring path starts with /wp-json/wc/v3
// We assume _baseUrl is just the domain e.g. https://site.com
// We append the internal implementation path
return Uri.parse('$_baseUrl/wp-json/wc/v3$path').replace(queryParameters: queryParams);
}
Future<List<dynamic>> getProducts({int page = 1, String search = ''}) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products', {
'page': page.toString(),
'per_page': '20',
'search': search,
});
final response = await http.get(uri, headers: _headers);
if (response.statusCode == 200) {
try {
return jsonDecode(response.body);
} on FormatException {
throw Exception('Invalid JSON response');
}
} else {
throw Exception('Failed to load products: ${response.statusCode}');
}
}
Future<Map<String, dynamic>> getProduct(dynamic id) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products/$id');
final response = await http.get(uri, headers: _headers);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to load product');
}
}
Future<void> createProduct(Map<String, dynamic> productData) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products');
final response = await http.post(
uri,
headers: _headers,
body: jsonEncode(productData),
);
if (response.statusCode != 200 && response.statusCode != 201) {
throw Exception('Failed to create product: ${response.body}');
}
}
Future<void> updateProduct(int id, Map<String, dynamic> productData) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products/$id');
final response = await http.put(
uri,
headers: _headers,
body: jsonEncode(productData),
);
if (response.statusCode != 200) {
throw Exception('Failed to update product: ${response.body}');
}
}
Future<void> deleteProduct(int id) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products/$id', {'force': 'true'});
final response = await http.delete(uri, headers: _headers);
if (response.statusCode != 200) {
throw Exception('Failed to delete product');
}
}
}

91
mobile_app/lib/main.dart Normal file
View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'api_service.dart';
import 'profile_manager.dart'; // Import ProfileManager
import 'screens/profile_login_screen.dart'; // Change import
import 'screens/site_selection_screen.dart'; // New Import
import 'screens/product_list_screen.dart';
import 'screens/product_editor_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider( // Use MultiProvider
providers: [
ChangeNotifierProvider(create: (_) => ApiService()),
ChangeNotifierProvider(create: (_) => ProfileManager()),
],
child: MaterialApp(
title: 'Inventory Manager',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
background: const Color(0xFF111827),
surface: const Color(0xFF1F2937),
),
scaffoldBackgroundColor: const Color(0xFF111827),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF1F2937),
elevation: 0,
centerTitle: false,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFF374151),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
hintStyle: const TextStyle(color: Colors.grey),
),
),
home: const AuthWrapper(),
routes: {
'/product/new': (context) => const ProductEditorScreen(),
},
onGenerateRoute: (settings) {
if (settings.name == '/product/edit') {
final product = settings.arguments as Map<String, dynamic>;
return MaterialPageRoute(
builder: (context) => ProductEditorScreen(product: product),
);
}
return null;
},
),
);
}
}
class AuthWrapper extends StatelessWidget {
const AuthWrapper({super.key});
@override
Widget build(BuildContext context) {
// Current Flow:
// 1. App Start
// 2. Check ProfileManager.isAuthenticated (PIN Protection)
// NO -> ProfileLoginScreen
// YES -> SiteSelectionScreen
return Consumer<ProfileManager>(
builder: (context, profile, child) {
if (!profile.isAuthenticated) {
return const ProfileLoginScreen();
} else {
return const SiteSelectionScreen();
}
},
);
}
}

View File

@ -0,0 +1,101 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SiteConfig {
final String id;
final String name;
final String url;
final String consumerKey;
final String consumerSecret;
SiteConfig({
required this.id,
required this.name,
required this.url,
required this.consumerKey,
required this.consumerSecret,
});
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'url': url,
'consumerKey': consumerKey,
'consumerSecret': consumerSecret,
};
factory SiteConfig.fromJson(Map<String, dynamic> json) => SiteConfig(
id: json['id'],
name: json['name'],
url: json['url'],
consumerKey: json['consumerKey'],
consumerSecret: json['consumerSecret'],
);
}
class ProfileManager with ChangeNotifier {
bool _isAuthenticated = false;
bool _hasPin = false;
List<SiteConfig> _sites = [];
bool get isAuthenticated => _isAuthenticated;
bool get hasPin => _hasPin;
List<SiteConfig> get sites => _sites;
ProfileManager() {
_init();
}
Future<void> _init() async {
final prefs = await SharedPreferences.getInstance();
_hasPin = prefs.containsKey('app_pin');
final sitesJson = prefs.getStringList('saved_sites') ?? [];
_sites = sitesJson.map((s) => SiteConfig.fromJson(jsonDecode(s))).toList();
notifyListeners();
}
Future<void> setPin(String pin) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('app_pin', pin);
_hasPin = true;
_isAuthenticated = true; // Auto login on creation
notifyListeners();
}
Future<bool> unlock(String pin) async {
final prefs = await SharedPreferences.getInstance();
final storedPin = prefs.getString('app_pin');
if (storedPin == pin) {
_isAuthenticated = true;
notifyListeners();
return true;
}
return false;
}
Future<void> addSite(SiteConfig site) async {
_sites.add(site);
await _saveSites();
notifyListeners();
}
Future<void> removeSite(String id) async {
_sites.removeWhere((s) => s.id == id);
await _saveSites();
notifyListeners();
}
Future<void> _saveSites() async {
final prefs = await SharedPreferences.getInstance();
final sitesJson = _sites.map((s) => jsonEncode(s.toJson())).toList();
await prefs.setStringList('saved_sites', sitesJson);
}
void logout() {
_isAuthenticated = false;
notifyListeners();
}
}

View File

@ -0,0 +1,142 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import '../api_service.dart';
import '../profile_manager.dart';
class AddSiteScreen extends StatefulWidget {
const AddSiteScreen({super.key});
@override
State<AddSiteScreen> createState() => _AddSiteScreenState();
}
class _AddSiteScreenState extends State<AddSiteScreen> {
final _nameController = TextEditingController();
final _urlController = TextEditingController();
final _keyController = TextEditingController();
final _secretController = TextEditingController();
bool _isLoading = false;
String _getPlatformHint() {
if (kIsWeb) return 'e.g. localhost:8000';
if (defaultTargetPlatform == TargetPlatform.android) return 'e.g. 10.0.2.2:8000';
return 'e.g. localhost:8000';
}
void _handleSave() async {
if (_nameController.text.isEmpty ||
_urlController.text.isEmpty ||
_keyController.text.isEmpty ||
_secretController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please fill all fields')),
);
return;
}
setState(() => _isLoading = true);
try {
final api = Provider.of<ApiService>(context, listen: false);
final profile = Provider.of<ProfileManager>(context, listen: false);
// Verify Direct Connection
await api.verifyCredentials(
_urlController.text,
_keyController.text,
_secretController.text
);
// Normalize Target URL for saving
String targetUrl = _urlController.text.trim();
if (!targetUrl.startsWith('http')) targetUrl = 'https://$targetUrl';
if (targetUrl.endsWith('/')) targetUrl = targetUrl.substring(0, targetUrl.length - 1);
await profile.addSite(SiteConfig(
id: const Uuid().v4(),
name: _nameController.text,
url: targetUrl,
consumerKey: _keyController.text,
consumerSecret: _secretController.text,
));
if (mounted) {
Navigator.pop(context);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Connection failed: $e'), backgroundColor: Colors.red),
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Add Store')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
TextField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Store Name',
hintText: 'e.g. Shoe Store',
prefixIcon: Icon(Icons.store),
),
),
const SizedBox(height: 16),
TextField(
controller: _urlController,
decoration: InputDecoration(
labelText: 'Store URL',
hintText: _getPlatformHint(),
prefixIcon: const Icon(Icons.link),
),
),
const SizedBox(height: 16),
TextField(
controller: _keyController,
decoration: const InputDecoration(
labelText: 'Consumer Key',
prefixIcon: Icon(Icons.key),
),
),
const SizedBox(height: 16),
TextField(
controller: _secretController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Consumer Secret',
prefixIcon: Icon(Icons.lock),
),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleSave,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: _isLoading
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
: const Text('Verify & Save'),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,284 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../api_service.dart';
class ProductEditorScreen extends StatefulWidget {
final Map<String, dynamic>? product;
const ProductEditorScreen({super.key, this.product});
@override
State<ProductEditorScreen> createState() => _ProductEditorScreenState();
}
class _ProductEditorScreenState extends State<ProductEditorScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
final _formKey = GlobalKey<FormState>();
bool _isSaving = false;
// Controllers
final _nameController = TextEditingController();
final _priceController = TextEditingController();
final _skuController = TextEditingController();
final _descController = TextEditingController();
final _shortDescController = TextEditingController();
final _stockController = TextEditingController();
bool _manageStock = false;
String _stockStatus = 'instock';
List<dynamic> _metaData = [];
bool get _isNew => widget.product == null;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_initializeData();
}
void _initializeData() {
if (widget.product != null) {
final p = widget.product!;
_nameController.text = p['name'] ?? '';
_priceController.text = p['regular_price'] ?? '';
_skuController.text = p['sku'] ?? '';
_descController.text = p['description']?.replaceAll(RegExp(r'<[^>]*>'), '') ?? ''; // Strip HTML for now
_shortDescController.text = p['short_description']?.replaceAll(RegExp(r'<[^>]*>'), '') ?? '';
_stockController.text = (p['stock_quantity'] ?? 0).toString();
_manageStock = p['manage_stock'] == true;
_stockStatus = p['stock_status'] ?? 'instock';
_metaData = List.from(p['meta_data'] ?? []);
}
}
@override
void dispose() {
_tabController.dispose();
_nameController.dispose();
_priceController.dispose();
_skuController.dispose();
_descController.dispose();
_shortDescController.dispose();
_stockController.dispose();
super.dispose();
}
Future<void> _saveProduct() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isSaving = true);
final api = Provider.of<ApiService>(context, listen: false);
final productData = {
'name': _nameController.text,
'regular_price': _priceController.text,
'sku': _skuController.text,
'description': _descController.text,
'short_description': _shortDescController.text,
'manage_stock': _manageStock,
'stock_quantity': int.tryParse(_stockController.text) ?? 0,
'stock_status': _stockStatus,
'meta_data': _metaData,
};
try {
if (_isNew) {
await api.createProduct(productData);
} else {
await api.updateProduct(widget.product!['id'], productData);
}
if (mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Product saved successfully')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString()), backgroundColor: Colors.red),
);
}
} finally {
if (mounted) setState(() => _isSaving = false);
}
}
void _addMetaField() {
setState(() {
_metaData.add({'key': '', 'value': ''});
});
}
void _removeMetaField(int index) {
setState(() {
_metaData.removeAt(index);
});
}
void _updateMetaField(int index, String key, String value) {
setState(() {
// We modify the map in place or create new
_metaData[index] = {'key': key, 'value': value};
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_isNew ? 'New Product' : 'Edit Product'),
actions: [
IconButton(
icon: const Icon(Icons.check),
onPressed: _isSaving ? null : _saveProduct,
)
],
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'General'),
Tab(text: 'Inventory'),
Tab(text: 'Plugins'),
],
),
),
body: Form(
key: _formKey,
child: TabBarView(
controller: _tabController,
children: [
// General Tab
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Product Name'),
validator: (v) => v!.isEmpty ? 'Required' : null,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(child: TextFormField(
controller: _priceController,
decoration: const InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.number,
)),
const SizedBox(width: 16),
Expanded(child: TextFormField(
controller: _skuController,
decoration: const InputDecoration(labelText: 'SKU'),
)),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _shortDescController,
decoration: const InputDecoration(labelText: 'Short Description'),
maxLines: 2,
),
const SizedBox(height: 16),
TextFormField(
controller: _descController,
decoration: const InputDecoration(labelText: 'Description'),
maxLines: 5,
),
],
),
),
// Inventory Tab
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
SwitchListTile(
title: const Text('Manage Stock'),
value: _manageStock,
onChanged: (v) => setState(() => _manageStock = v),
),
if (_manageStock)
TextFormField(
controller: _stockController,
decoration: const InputDecoration(labelText: 'Stock Quantity'),
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _stockStatus,
decoration: const InputDecoration(labelText: 'Stock Status'),
items: const [
DropdownMenuItem(value: 'instock', child: Text('In Stock')),
DropdownMenuItem(value: 'outofstock', child: Text('Out of Stock')),
DropdownMenuItem(value: 'onbackorder', child: Text('On Backorder')),
],
onChanged: (v) => setState(() => _stockStatus = v!),
),
],
),
),
// Plugins / Meta Tab
ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _metaData.length + 1,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
if (index == _metaData.length) {
return TextButton.icon(
onPressed: _addMetaField,
icon: const Icon(Icons.add),
label: const Text('Add Custom Meta'),
);
}
final meta = _metaData[index];
final key = meta['key']?.toString() ?? '';
final value = meta['value']?.toString() ?? '';
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade800),
borderRadius: BorderRadius.circular(8),
color: const Color(0xFF1F2937),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(child: TextFormField(
initialValue: key,
decoration: const InputDecoration(labelText: 'Key', contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 4)),
style: const TextStyle(fontSize: 12),
onChanged: (v) => _updateMetaField(index, v, value),
)),
IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () => _removeMetaField(index),
)
],
),
const SizedBox(height: 8),
TextFormField(
initialValue: value,
decoration: const InputDecoration(labelText: 'Value', contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8)),
maxLines: null,
onChanged: (v) => _updateMetaField(index, key, v),
),
],
),
);
},
),
],
),
),
);
}
}

View File

@ -0,0 +1,271 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../api_service.dart';
class ProductListScreen extends StatefulWidget {
const ProductListScreen({super.key});
@override
State<ProductListScreen> createState() => _ProductListScreenState();
}
class _ProductListScreenState extends State<ProductListScreen> {
final _scrollController = ScrollController();
final _searchController = TextEditingController();
List<dynamic> _products = [];
bool _isLoading = false;
int _page = 1;
bool _hasMore = true;
String _searchQuery = '';
@override
void initState() {
super.initState();
_fetchProducts();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.dispose();
_searchController.dispose();
super.dispose();
}
void _onScroll() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200 &&
!_isLoading &&
_hasMore) {
_fetchProducts();
}
}
Future<void> _fetchProducts({bool refresh = false}) async {
if (_isLoading) return;
setState(() {
_isLoading = true;
if (refresh) {
_page = 1;
_products.clear();
_hasMore = true;
}
});
try {
final newProducts = await Provider.of<ApiService>(context, listen: false)
.getProducts(page: _page, search: _searchQuery);
setState(() {
_products.addAll(newProducts);
_page++;
if (newProducts.isEmpty || newProducts.length < 10) { // Assuming page size 10
_hasMore = false;
}
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to load products: $e')),
);
} finally {
setState(() => _isLoading = false);
}
}
void _handleSearch(String query) {
if (_searchQuery == query) return;
_searchQuery = query;
_fetchProducts(refresh: true);
}
void _deleteProduct(int id) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Product'),
content: const Text('Are you sure you want to delete this product?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete', style: TextStyle(color: Colors.red))
),
],
),
);
if (confirmed == true) {
try {
await Provider.of<ApiService>(context, listen: false).deleteProduct(id);
setState(() {
_products.removeWhere((p) => p['id'] == id);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Product deleted')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to delete: $e')),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Inventory'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => Provider.of<ApiService>(context, listen: false).logout(),
)
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(60),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
onSubmitted: _handleSearch,
decoration: InputDecoration(
hintText: 'Search products...',
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_handleSearch('');
},
),
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await Navigator.pushNamed(context, '/product/new');
_fetchProducts(refresh: true);
},
backgroundColor: Colors.blue,
child: const Icon(Icons.add),
),
body: RefreshIndicator(
onRefresh: () => _fetchProducts(refresh: true),
child: _products.isEmpty && !_isLoading
? const Center(child: Text('No products found'))
: GridView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 300,
childAspectRatio: 0.75,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: _products.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == _products.length) {
return const Center(child: CircularProgressIndicator());
}
final product = _products[index];
final imageUrl = (product['images'] as List).isNotEmpty
? product['images'][0]['src']
: null;
return GestureDetector(
onTap: () async {
await Navigator.pushNamed(
context,
'/product/edit',
arguments: product,
);
_fetchProducts(refresh: true);
},
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: const Color(0xFF1F2937),
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: Stack(
fit: StackFit.expand,
children: [
imageUrl != null
? Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
const Center(child: Icon(Icons.broken_image, size: 40)),
)
: const Center(child: Icon(Icons.image, size: 40, color: Colors.grey)),
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: product['stock_status'] == 'instock'
? Colors.green.withOpacity(0.9)
: Colors.red.withOpacity(0.9),
borderRadius: BorderRadius.circular(12),
),
child: Text(
product['stock_status'] == 'instock' ? 'In Stock' : 'Out',
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white),
),
),
),
],
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
product['name'],
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'\$${product['price']}',
style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
),
InkWell(
onTap: () => _deleteProduct(product['id']),
child: const Icon(Icons.delete_outline, color: Colors.red, size: 20),
)
],
),
],
),
),
),
],
),
),
);
},
),
),
);
}
}

View File

@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../profile_manager.dart';
class ProfileLoginScreen extends StatefulWidget {
const ProfileLoginScreen({super.key});
@override
State<ProfileLoginScreen> createState() => _ProfileLoginScreenState();
}
class _ProfileLoginScreenState extends State<ProfileLoginScreen> {
final _pinController = TextEditingController();
final _confirmController = TextEditingController();
bool _isCreating = false;
String _error = '';
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Check if we need to create a PIN
final profile = Provider.of<ProfileManager>(context, listen: false);
// Note: profile status might update async, so we rely on Consumer in AuthWrapper usually,
// but here we are inside the screen.
// Actually, AuthWrapper decides if we show this screen.
// If we are here, it means !isAuthenticated.
// We check hasPin to decide mode.
}
void _handleSubmit() async {
final profile = Provider.of<ProfileManager>(context, listen: false);
if (_pinController.text.length < 4) {
setState(() => _error = 'PIN must be at least 4 digits');
return;
}
if (!profile.hasPin) {
// Creating Request
if (!_isCreating) {
// Ask for confirmation
setState(() {
_isCreating = true;
_error = '';
});
return;
}
if (_pinController.text != _confirmController.text) {
setState(() => _error = 'PINs do not match');
return;
}
await profile.setPin(_pinController.text);
} else {
// Unlocking
final success = await profile.unlock(_pinController.text);
if (!success) {
setState(() => _error = 'Incorrect PIN');
}
}
}
@override
Widget build(BuildContext context) {
// Watch hasPin to react if it changes (rare here but good practice)
final hasPin = Provider.of<ProfileManager>(context).hasPin;
return Scaffold(
body: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.lock_outline, size: 64, color: Colors.blue),
const SizedBox(height: 24),
Text(
hasPin ? 'Enter Master PIN' : (_isCreating ? 'Confirm Master PIN' : 'Create Master PIN'),
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
hasPin
? 'Unlock your secure profiles'
: 'Protect your store credentials',
style: const TextStyle(color: Colors.grey),
),
const SizedBox(height: 32),
TextField(
controller: _pinController,
obscureText: true,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'PIN',
prefixIcon: Icon(Icons.password),
),
onSubmitted: (_) => _isCreating ? null : _handleSubmit(), // If confirming, focus next or submit
),
if (_isCreating) ...[
const SizedBox(height: 16),
TextField(
controller: _confirmController,
obscureText: true,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Confirm PIN',
prefixIcon: Icon(Icons.check),
),
onSubmitted: (_) => _handleSubmit(),
),
],
if (_error.isNotEmpty) ...[
const SizedBox(height: 16),
Text(_error, style: const TextStyle(color: Colors.red)),
],
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _handleSubmit,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: Text(hasPin ? 'Unlock' : (_isCreating ? 'Save PIN' : 'Next')),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../profile_manager.dart';
import '../api_service.dart';
import 'add_site_screen.dart';
import 'product_list_screen.dart';
class SiteSelectionScreen extends StatelessWidget {
const SiteSelectionScreen({super.key});
@override
Widget build(BuildContext context) {
final profile = Provider.of<ProfileManager>(context);
final api = Provider.of<ApiService>(context);
return Scaffold(
appBar: AppBar(
title: const Text('My Stores'),
actions: [
IconButton(
icon: const Icon(Icons.lock),
onPressed: () => profile.logout(),
tooltip: 'Lock App',
)
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const AddSiteScreen()));
},
child: const Icon(Icons.add),
),
body: profile.sites.isEmpty
? const Center(child: Text('No sites added. Tap + to add one.'))
: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: profile.sites.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final site = profile.sites[index];
return Card(
color: const Color(0xFF1F2937),
child: ListTile(
contentPadding: const EdgeInsets.all(16),
leading: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(Icons.store, color: Colors.blue),
),
title: Text(site.name, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(site.url, style: const TextStyle(color: Colors.grey)),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () {
// Connect API Directly
api.connect(site.url, site.consumerKey, site.consumerSecret);
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ProductListWrapper()),
);
},
onLongPress: () {
// Option to delete
showDialog(context: context, builder: (_) => AlertDialog(
title: const Text('Delete Site'),
content: Text('Remove ${site.name}?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
TextButton(onPressed: () {
profile.removeSite(site.id);
Navigator.pop(context);
}, child: const Text('Delete', style: TextStyle(color: Colors.red))),
],
));
},
),
);
},
),
);
}
}
// Wrapper to listen to API changes (like logout from within ProductList)
class ProductListWrapper extends StatelessWidget {
const ProductListWrapper({super.key});
@override
Widget build(BuildContext context) {
return Consumer<ApiService>(
builder: (context, api, _) {
if (!api.isLoggedIn) {
// If we logged out (disconnected), pop back to selection
// We need to do this carefully during build...
// Better: The logout button in ProductList should just api.disconnect() AND Navigator.pop().
// So here we likely just show ProductList.
return const ProductListScreen();
}
return const ProductListScreen();
},
);
}
}

1
mobile_app/linux/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
flutter/ephemeral

View File

@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "mobile_app")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.mobile_app")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

Some files were not shown because too many files have changed in this diff Show More