first commit
This commit is contained in:
commit
e7ddf755ea
6
.env.development
Normal file
6
.env.development
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CLIENT_BASE_PATH=/
|
||||||
|
CLIENT_API_URL=https://localhost:7205
|
||||||
|
CLIENT_APP_NAME=Merchant Operator App
|
||||||
|
CLIENT_DEBUG=true
|
||||||
|
CLIENT_MAP_TILE_ENDPOINT=https://tiles.locationiq.com/v3/streets/vector.json
|
||||||
|
CLIENT_MAP_TILE_API_KEY=pk.19d62daa998822ac22861d96a0424b58
|
6
.env.template
Normal file
6
.env.template
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CLIENT_BASE_PATH=/
|
||||||
|
CLIENT_API_URL=https://
|
||||||
|
CLIENT_APP_NAME=Merchant Operator App
|
||||||
|
CLIENT_DEBUG=true
|
||||||
|
CLIENT_MAP_TILE_ENDPOINT=https://
|
||||||
|
CLIENT_MAP_TILE_API_KEY=
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/node_modules/
|
||||||
|
/dist/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
docker-compose.override.yml
|
3
.husky/commit-msg
Normal file
3
.husky/commit-msg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
npx --no -- commitlint --edit ${1}
|
3
.husky/pre-commit
Normal file
3
.husky/pre-commit
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
npx lint-staged
|
1
.npmrc
Normal file
1
.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
@g1:registry=https://git.generation.one/api/packages/GenerationOne/npm/
|
33
.vscode/launch.json
vendored
Normal file
33
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Debug Vite (server + browser)",
|
||||||
|
"configurations": [
|
||||||
|
"Launch Vite Dev Server",
|
||||||
|
"Launch Brave against localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Brave against localhost",
|
||||||
|
"type": "pwa-chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:5173",
|
||||||
|
"webRoot": "${workspaceFolder}/src",
|
||||||
|
"runtimeExecutable": "C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pwa-node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Vite Dev Server",
|
||||||
|
"program": "${workspaceFolder}/node_modules/vite/bin/vite.js",
|
||||||
|
"args": ["--port", "5173"],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"autoAttachChildProcesses": true,
|
||||||
|
"skipFiles": ["<node_internals>/**"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:slim AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build -- --base=./
|
||||||
|
|
||||||
|
# NOTE: We copy the environment variables CLIENT_ to an .env file in the running container.
|
||||||
|
# This is done with the docker-entrypoint.sh script.
|
||||||
|
# We do this so one build can support many different environments.
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
RUN sed -i 's/\r$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 369
|
||||||
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
96
README.md
Normal file
96
README.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# Merchant Operator Web
|
||||||
|
|
||||||
|
Merchant Operator App is a browser-only React SPA that lets local businesses create, edit, and audit their own merchant records on Local—a community-commerce network running on the peer-to-peer FogBox stack. Its mission is to decentralize ownership of commercial data and return technological control to merchants and their customers. Data is written locally first, then synchronised through FogBox nodes—no central server—ensuring autonomy and resilience. Open, well-documented APIs let any citizen-developer extend, fork, or integrate the codebase without gatekeepers.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This project follows a Flux-style architecture using Effector for state management. The architecture is organized around domains, with each domain containing its model (events, stores, effects), view-model (React hooks), and UI components.
|
||||||
|
|
||||||
|
For more details, see the [Architecture Documentation](./docs/architecture.md).
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
The project follows a domain-driven structure for better maintainability and scalability:
|
||||||
|
|
||||||
|
```
|
||||||
|
merchant-operator-web/
|
||||||
|
├── src/
|
||||||
|
│ ├── app/ # Application entry point and configuration
|
||||||
|
│ │ ├── root.tsx # Root component
|
||||||
|
│ │ └── routes.tsx # Route definitions
|
||||||
|
│ ├── domains/ # Feature domains
|
||||||
|
│ │ └── <feature>/ # e.g., products, auth, orders
|
||||||
|
│ │ ├── model.ts # Effector events, stores, effects
|
||||||
|
│ │ ├── view-model.ts # React hooks
|
||||||
|
│ │ └── ui/ # React components
|
||||||
|
│ ├── shared/ # Shared code
|
||||||
|
│ │ ├── lib/ # Utilities, helpers, adapters
|
||||||
|
│ │ └── ui/ # Reusable UI components
|
||||||
|
│ ├── assets/ # Static assets (images, fonts)
|
||||||
|
│ ├── styles/ # Global styles
|
||||||
|
│ └── types/ # TypeScript types
|
||||||
|
├── public/ # Static files
|
||||||
|
├── config/ # Build/env configuration
|
||||||
|
└── docs/ # Documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Path Aliases
|
||||||
|
|
||||||
|
The project uses path aliases for cleaner imports:
|
||||||
|
|
||||||
|
- `@` - Points to the `src` directory
|
||||||
|
- `@app` - Points to the `src/app` directory
|
||||||
|
- `@domains` - Points to the `src/domains` directory
|
||||||
|
- `@shared` - Points to the `src/shared` directory
|
||||||
|
- `@shared/ui` - Points to the `src/shared/ui` directory
|
||||||
|
- `@shared/lib` - Points to the `src/shared/lib` directory
|
||||||
|
- `@assets` - Points to the `src/assets` directory
|
||||||
|
- `@styles` - Points to the `src/styles` directory
|
||||||
|
- `@types` - Points to the `src/types` directory
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js (v18+)
|
||||||
|
- npm or yarn
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Development Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Preview Production Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The application uses runtime environment variables for configuration, which are injected into the application through `window.appConfig`.
|
||||||
|
|
||||||
|
For detailed configuration options, see the [Configuration Documentation](./docs/configuration.md).
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
For more detailed documentation, see the [docs](./docs) directory:
|
||||||
|
|
||||||
|
- [Architecture](./docs/architecture.md) - Overview of our Flux-style architecture with Effector
|
||||||
|
- [Project Structure](./docs/project-structure.md) - Detailed explanation of our project structure
|
||||||
|
- [Configuration](./docs/configuration.md) - Detailed configuration options and environment variables
|
||||||
|
- [Effector Guide](./docs/effector-guide.md) - Comprehensive guide on how to use Effector
|
||||||
|
- [TypeScript Conventions](./docs/typescript-conventions.md) - TypeScript conventions for the project
|
7
babel.config.js
Normal file
7
babel.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
["@babel/preset-env", { targets: { browsers: "last 2 versions" } }],
|
||||||
|
["@babel/preset-react", { runtime: "automatic" }],
|
||||||
|
],
|
||||||
|
plugins: ["@babel/plugin-transform-react-jsx"],
|
||||||
|
};
|
36
commitlint.config.js
Normal file
36
commitlint.config.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export default {
|
||||||
|
extends: ["@commitlint/config-conventional"],
|
||||||
|
rules: {
|
||||||
|
"body-leading-blank": [1, "always"],
|
||||||
|
"body-max-line-length": [2, "always", 100],
|
||||||
|
"footer-leading-blank": [1, "always"],
|
||||||
|
"footer-max-line-length": [2, "always", 100],
|
||||||
|
"header-max-length": [2, "always", 100],
|
||||||
|
"subject-case": [
|
||||||
|
2,
|
||||||
|
"never",
|
||||||
|
["sentence-case", "start-case", "pascal-case", "upper-case"],
|
||||||
|
],
|
||||||
|
"subject-empty": [2, "never"],
|
||||||
|
"subject-full-stop": [2, "never", "."],
|
||||||
|
"type-case": [2, "always", "lower-case"],
|
||||||
|
"type-empty": [2, "never"],
|
||||||
|
"type-enum": [
|
||||||
|
2,
|
||||||
|
"always",
|
||||||
|
[
|
||||||
|
"build",
|
||||||
|
"chore",
|
||||||
|
"ci",
|
||||||
|
"docs",
|
||||||
|
"feat",
|
||||||
|
"fix",
|
||||||
|
"perf",
|
||||||
|
"refactor",
|
||||||
|
"revert",
|
||||||
|
"style",
|
||||||
|
"test",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
12
docker-compose.override.yml.template
Normal file
12
docker-compose.override.yml.template
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
merchant-operator-web:
|
||||||
|
environment:
|
||||||
|
- CLIENT_BASE_PATH=
|
||||||
|
- CLIENT_API_URL=https://
|
||||||
|
- CLIENT_APP_NAME=
|
||||||
|
- CLIENT_DEBUG=
|
||||||
|
- CLIENT_MAP_TILE_ENDPOINT=https://
|
||||||
|
- CLIENT_MAP_TILE_API_KEY=
|
||||||
|
networks:
|
||||||
|
traefik: # ← key stays literal, no variables here
|
||||||
|
external: true # shorthand; Compose will look for a network literally named “traefik”
|
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
services:
|
||||||
|
merchant-operator-web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
container_name: ${APP_CONTAINER_NAME:-merchant-operator-web}
|
||||||
|
environment:
|
||||||
|
- CLIENT_BASE_PATH=${CLIENT_BASE_PATH:-/}
|
||||||
|
- CLIENT_API_URL=${CLIENT_API_URL:-}
|
||||||
|
- CLIENT_APP_NAME=${CLIENT_APP_NAME:-Merchant Operator App}
|
||||||
|
- CLIENT_DEBUG=${CLIENT_DEBUG:-false}
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.${APP_NAME:-merchant-operator-web}.rule=Host(`${APP_DOMAIN:-localhost}`) && PathPrefix(`${BASE_PATH:-/}`)"
|
||||||
|
- "traefik.http.routers.${APP_NAME:-merchant-operator-web}.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}"
|
||||||
|
- "traefik.http.routers.${APP_NAME:-merchant-operator-web}.tls=${ENABLE_TLS:-false}"
|
||||||
|
- "traefik.http.routers.${APP_NAME:-merchant-operator-web}.tls.certresolver=${CERT_RESOLVER:-letsencrypt}"
|
||||||
|
- "traefik.http.services.${APP_NAME:-merchant-operator-web}.loadbalancer.server.port=369"
|
||||||
|
- "traefik.http.routers.${APP_NAME:-merchant-operator-web}.middlewares=${APP_NAME:-merchant-operator-web}-strip"
|
||||||
|
- "traefik.http.middlewares.${APP_NAME:-merchant-operator-web}-strip.stripprefix.prefixes=${BASE_PATH:-/}"
|
129
docker-entrypoint.sh
Normal file
129
docker-entrypoint.sh
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# docker-entrypoint.sh
|
||||||
|
# ────────────────────
|
||||||
|
# Creates /usr/share/nginx/html/app-config.js from CLIENT_* env‑vars
|
||||||
|
# and then starts Nginx.
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Starting docker-entrypoint.sh script..."
|
||||||
|
echo "Current directory: $(pwd)"
|
||||||
|
echo "Preparing runtime environment..."
|
||||||
|
|
||||||
|
TARGET_DIR="/usr/share/nginx/html"
|
||||||
|
|
||||||
|
echo "Checking target directory: $TARGET_DIR"
|
||||||
|
if [ -d "$TARGET_DIR" ]; then
|
||||||
|
echo "Target directory exists"
|
||||||
|
ls -la "$TARGET_DIR"
|
||||||
|
else
|
||||||
|
echo "ERROR: Target directory does not exist: $TARGET_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating runtime environment variables script..."
|
||||||
|
|
||||||
|
if [ -d "$TARGET_DIR" ]; then
|
||||||
|
TEMP_DIR="/tmp/app-config"
|
||||||
|
mkdir -p "$TEMP_DIR"
|
||||||
|
rm -rf "$TEMP_DIR"/*
|
||||||
|
|
||||||
|
#
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
# 1. Robust camel‑case conversion ← *changed*
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
to_camel_case() {
|
||||||
|
# Converts FOO_BAR_BAZ → fooBarBaz (POSIX‑sh + awk)
|
||||||
|
printf '%s\n' "$1" | awk -F'_' '{
|
||||||
|
for (i = 1; i <= NF; i++) {
|
||||||
|
if (i == 1) {
|
||||||
|
# first chunk stays lower‑case
|
||||||
|
printf tolower($i)
|
||||||
|
} else {
|
||||||
|
# capitalise 1st, lower‑case rest
|
||||||
|
printf toupper(substr($i,1,1)) tolower(substr($i,2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Processing CLIENT_ environment variables..."
|
||||||
|
env | grep '^CLIENT_' | while IFS='=' read -r key value; do
|
||||||
|
key_without_prefix="${key#CLIENT_}"
|
||||||
|
camel_key="$(to_camel_case "$key_without_prefix")"
|
||||||
|
echo "$value" > "$TEMP_DIR/$camel_key"
|
||||||
|
echo " Processed $key -> $camel_key"
|
||||||
|
done
|
||||||
|
|
||||||
|
#
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
# 2. Add defaults only if the key is still missing ← *changed*
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
[ -f "$TEMP_DIR/apiUrl" ] || echo "https://localhost:7205" >"$TEMP_DIR/apiUrl"
|
||||||
|
[ -f "$TEMP_DIR/basePath" ] || echo "/" >"$TEMP_DIR/basePath"
|
||||||
|
[ -f "$TEMP_DIR/appName" ] || echo "Merchant Operator App" >"$TEMP_DIR/appName"
|
||||||
|
[ -f "$TEMP_DIR/debug" ] || echo "false" >"$TEMP_DIR/debug"
|
||||||
|
|
||||||
|
echo "Generating app-config.js..."
|
||||||
|
{
|
||||||
|
echo "// Runtime environment variables - Generated by docker-entrypoint.sh"
|
||||||
|
echo "window.appConfig = {"
|
||||||
|
for file in "$TEMP_DIR"/*; do
|
||||||
|
key="$(basename "$file")"
|
||||||
|
value="$(cat "$file")"
|
||||||
|
if echo "$value" | grep -Eq '^(true|false|null|[0-9]+)$'; then
|
||||||
|
echo " $key: $value,"
|
||||||
|
else
|
||||||
|
# escape double quotes just in case
|
||||||
|
esc_value=$(printf '%s' "$value" | sed 's/"/\\"/g')
|
||||||
|
echo " $key: \"$esc_value\","
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "};"
|
||||||
|
} > "$TARGET_DIR/app-config.js"
|
||||||
|
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
chmod 644 "$TARGET_DIR/app-config.js"
|
||||||
|
|
||||||
|
if [ -f "$TARGET_DIR/index.html" ]; then
|
||||||
|
if ! grep -q "app-config.js" "$TARGET_DIR/index.html"; then
|
||||||
|
sed -i 's|<head>|<head>\n <script src="/app-config.js"></script>|' "$TARGET_DIR/index.html"
|
||||||
|
echo "Added app-config.js script to index.html"
|
||||||
|
else
|
||||||
|
echo "app-config.js script already exists in index.html"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "ERROR: index.html not found in $TARGET_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Runtime environment variables script created successfully."
|
||||||
|
else
|
||||||
|
echo "ERROR: Target directory does not exist: $TARGET_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Environment variables extracted. Starting Nginx server..."
|
||||||
|
|
||||||
|
echo "Checking Nginx configuration..."
|
||||||
|
if [ -f "/etc/nginx/conf.d/default.conf" ]; then
|
||||||
|
echo "Nginx configuration found at: /etc/nginx/conf.d/default.conf"
|
||||||
|
cat /etc/nginx/conf.d/default.conf
|
||||||
|
else
|
||||||
|
echo "ERROR: Nginx configuration not found at /etc/nginx/conf.d/default.conf"
|
||||||
|
ls -la /etc/nginx/conf.d/
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v nginx >/dev/null 2>&1; then
|
||||||
|
echo "Nginx found at: $(which nginx)"
|
||||||
|
nginx -v 2>&1
|
||||||
|
echo "Testing Nginx configuration..."
|
||||||
|
nginx -t
|
||||||
|
echo "Starting Nginx with: nginx -g 'daemon off;'"
|
||||||
|
exec nginx -g 'daemon off;'
|
||||||
|
else
|
||||||
|
echo "ERROR: Nginx not found or not executable"
|
||||||
|
exit 1
|
||||||
|
fi
|
45
docs/README.md
Normal file
45
docs/README.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Merchant Operator App Documentation
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Merchant Operator App is a browser-only React SPA for local businesses to manage their merchant records on a decentralized commerce network. It uses a peer-to-peer architecture with local-first data storage and synchronization through FogBox nodes.
|
||||||
|
|
||||||
|
## Agent Guidelines
|
||||||
|
|
||||||
|
- **Architecture**: Use Flux-style with Effector for state management
|
||||||
|
- **File Structure**: Follow domain-driven organization (model.ts, view-model.ts, ui/)
|
||||||
|
- **TypeScript**: Use strict mode with proper typing
|
||||||
|
- **Components**: Keep UI components pure and presentational
|
||||||
|
- **State Management**: Use Effector events, stores, and effects
|
||||||
|
- **API Models**: Use the generated DTOs from the API client
|
||||||
|
- **Naming**: Use $ prefix for stores, Fx suffix for effects
|
||||||
|
- **Exports**: Use named exports only, avoid default exports
|
||||||
|
|
||||||
|
## Architecture & Project Structure
|
||||||
|
|
||||||
|
- [Architecture](./architecture.md) - Overview of our Flux-style architecture with Effector
|
||||||
|
- [Flux vs. MVVM](./flux-vs-mvvm.md) - Comparison between Flux and MVVM architectures
|
||||||
|
- [Project Structure](./project-structure.md) - Detailed explanation of our project structure
|
||||||
|
- [Effector Guide](./effector-guide.md) - Comprehensive guide on how to use Effector
|
||||||
|
- [TypeScript Conventions](./typescript-conventions.md) - TypeScript conventions for the project
|
||||||
|
|
||||||
|
## Deployment & Development
|
||||||
|
|
||||||
|
- [Deployment Guide](./deployment.md) - Instructions for deploying the application with path-agnostic configuration
|
||||||
|
- [Local Development Guide](./local-development.md) - Instructions for running the application locally with different base paths
|
||||||
|
- [Client-Side Environment Variables](./client-env-vars.md) - Documentation for client-side environment variables
|
||||||
|
|
||||||
|
## Components & Utilities
|
||||||
|
|
||||||
|
- [Utility Functions](./utils.md) - Documentation for utility functions used in the application
|
||||||
|
- [NEW Components](./new-components.md) - Guide for using and adding components from the NEW directory
|
||||||
|
|
||||||
|
## Path-Agnostic SPA Deployment
|
||||||
|
|
||||||
|
The application is configured for path-agnostic deployment, allowing it to be served from any base path without requiring a rebuild. This is particularly useful for:
|
||||||
|
|
||||||
|
- Deploying to different environments (development, staging, production)
|
||||||
|
- Serving the application from a subdirectory
|
||||||
|
- Using the same build with different routing configurations
|
||||||
|
|
||||||
|
See the [Deployment Guide](./deployment.md) for detailed instructions on how to configure and deploy the application.
|
107
docs/VERSION_TAGGING.md
Normal file
107
docs/VERSION_TAGGING.md
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# Git Version Tagging Guide
|
||||||
|
|
||||||
|
This document outlines the process for managing version tags in the project's Git repository.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version Tagging Conventions
|
||||||
|
|
||||||
|
The project follows these versioning conventions:
|
||||||
|
|
||||||
|
- Production releases: `v0.0.X` (e.g., `v0.0.35`)
|
||||||
|
- Development releases: `v0.0.X-dev1` (e.g., `v0.0.34-dev1`)
|
||||||
|
- Format follows a modified semantic versioning pattern
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Incrementing Version Tags
|
||||||
|
|
||||||
|
Follow these steps to increment the version tag for a new release:
|
||||||
|
|
||||||
|
### 1. Check Existing Tags
|
||||||
|
|
||||||
|
List all existing version tags to understand the current versioning state:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git tag -l "v*"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Find the Latest Version Tag
|
||||||
|
|
||||||
|
For Windows PowerShell, use these commands to find the latest tags:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Find latest production tag (frank)
|
||||||
|
git tag -l "v0.0.*-frank" | Sort-Object -Property @{Expression={[int]($_ -replace '^v0.0.', '' -replace '-frank$', '')}; Descending=$true} | Select-Object -First 1
|
||||||
|
|
||||||
|
# Find latest development tag (dev)
|
||||||
|
git tag -l "v0.0.*-dev*" | Sort-Object -Property @{Expression={[int]($_ -replace '^v0.0.', '' -replace '-dev.*$', '')}; Descending=$true} | Select-Object -First 1
|
||||||
|
```
|
||||||
|
|
||||||
|
For Linux/macOS bash, use these commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find latest production tag (frank)
|
||||||
|
git tag -l "v0.0.*-frank" | sort -V | tail -1
|
||||||
|
|
||||||
|
# Find latest development tag (dev)
|
||||||
|
git tag -l "v0.0.*-dev*" | sort -V | tail -1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Increment the Version Number
|
||||||
|
|
||||||
|
Based on the latest tag, increment the version number according to the project's versioning scheme:
|
||||||
|
|
||||||
|
- If the latest tag is `v0.0.34-frank`, the new tag would be `v0.0.35-frank`
|
||||||
|
- For development versions, increment the version number only (e.g., `v0.0.34-dev1` → `v0.0.35-dev1`)
|
||||||
|
- Important: "dev1" is the channel identifier and remains constant; only increment the version number
|
||||||
|
- Example: `v0.0.87-dev1` → `v0.0.88-dev1` (correct)
|
||||||
|
- Example: `v0.0.87-dev1` → `v0.0.87-dev2` (incorrect)
|
||||||
|
|
||||||
|
### 4. Create a New Annotated Tag
|
||||||
|
|
||||||
|
Create a new annotated tag with a descriptive message:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git tag -a v0.0.35-frank -m "Description of changes in this version"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Push the New Tag to the Remote Repository
|
||||||
|
|
||||||
|
Push the new tag to the remote repository:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git push origin v0.0.35-frank
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **Use Annotated Tags**: Always use annotated tags (`-a` flag) for releases, as they contain metadata including the tagger's name, email, date, and a message.
|
||||||
|
|
||||||
|
- **Descriptive Messages**: Include a concise but descriptive message that summarizes the key changes in this version.
|
||||||
|
|
||||||
|
- **Consistent Naming**: Follow the established naming convention consistently.
|
||||||
|
|
||||||
|
- **Tag After Testing**: Create and push tags only after the code has been tested and confirmed to be working correctly.
|
||||||
|
|
||||||
|
- **Don't Reuse Tags**: Never reuse or move existing tags. If a mistake is made, delete the tag and create a new one.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deleting Tags (If Necessary)
|
||||||
|
|
||||||
|
If you need to delete a tag (e.g., if it was created incorrectly):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Delete local tag
|
||||||
|
git tag -d v0.0.35-frank
|
||||||
|
|
||||||
|
# Delete remote tag
|
||||||
|
git push origin :refs/tags/v0.0.35-frank
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_End of VERSION_TAGGING.md_
|
133
docs/architecture.md
Normal file
133
docs/architecture.md
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# Architecture: Flux-style with Effector
|
||||||
|
|
||||||
|
## 1. Terminology & Core Concepts
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
|------|------------|
|
||||||
|
| SPA (Single-Page Application) | Entire UI is boot-strapped once in the browser; further navigation is handled client-side. |
|
||||||
|
| Client-Side Rendering (CSR) | The browser runs JavaScript (React) that turns JSON/state into DOM; no HTML is streamed from the server after first load. |
|
||||||
|
| Effector | A small TypeScript-first library that implements Flux-style one-way data-flow with four primitives (event, store, effect, domain). |
|
||||||
|
| Flux-style architecture | Events mutate stores ➜ stores notify subscribed views; data always moves in one direction. |
|
||||||
|
| Effector-based Flux architecture | Using Effector as the single source of truth for state inside a React SPA. |
|
||||||
|
|
||||||
|
## 2. Effector Primitives → Flux Mapping
|
||||||
|
|
||||||
|
```
|
||||||
|
createEvent() ≈ Action
|
||||||
|
(create)Store() ≈ Store
|
||||||
|
createEffect() ≈ Async side-effect handler
|
||||||
|
Domains ≈ Namespace / module boundary
|
||||||
|
View ≈ React component using `useStore` / `useUnit`
|
||||||
|
```
|
||||||
|
|
||||||
|
Flow diagram:
|
||||||
|
|
||||||
|
```
|
||||||
|
event ─┐
|
||||||
|
│ triggers
|
||||||
|
store ──┤ update → React re-render
|
||||||
|
│
|
||||||
|
effect ←┘ (async; dispatches success/fail events)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Project Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
app/
|
||||||
|
root.tsx # <App/>, routing
|
||||||
|
routes.tsx # Route definitions
|
||||||
|
domains/
|
||||||
|
<feature>/
|
||||||
|
model.ts # Effector events, stores, effects (pure, testable)
|
||||||
|
view-model.ts # Thin hooks that adapt model to React
|
||||||
|
ui/ # Dumb presentational components
|
||||||
|
shared/
|
||||||
|
ui/ # Design-system atoms
|
||||||
|
lib/ # Generic helpers
|
||||||
|
tests/ # Test files
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Coding Guidelines
|
||||||
|
|
||||||
|
| Guideline | Why it helps |
|
||||||
|
|-----------|--------------|
|
||||||
|
| TypeScript strict mode | Types give static guarantees for safe edits. |
|
||||||
|
| Pure functions in model.ts | Deterministic behavior is easier to reason about. |
|
||||||
|
| Named exports only | Eliminates default-export rename hazards. |
|
||||||
|
| Small files, single responsibility | Enables chunked, parallel code transforms. |
|
||||||
|
| Unit tests co-located with domain logic | Provide regression oracles for automated changes. |
|
||||||
|
| Runtime schema checks (e.g., Zod) | Fail fast on unexpected data. |
|
||||||
|
|
||||||
|
## 5. Implementation Example
|
||||||
|
|
||||||
|
### model.ts
|
||||||
|
```typescript
|
||||||
|
import { createStore, createEvent, createEffect } from 'effector';
|
||||||
|
import { ProductsViewsService } from '@lib/api/merchant/services/ProductsViewsService';
|
||||||
|
import type { productCompositeDto } from '@lib/api/merchant/models/productCompositeDto';
|
||||||
|
|
||||||
|
// Define events
|
||||||
|
export const setProductsFilter = createEvent<Partial<productQueryRequestDto>>();
|
||||||
|
|
||||||
|
// Define effects
|
||||||
|
export const fetchProductsFx = createEffect(async (filter: productQueryRequestDto) => {
|
||||||
|
return await ProductsViewsService.postApiProductCompositeQueryByCanonical({
|
||||||
|
requestBody: filter
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create stores
|
||||||
|
export const $products = createStore<productCompositeDto[]>([])
|
||||||
|
.on(fetchProductsFx.doneData, (_, payload) => Array.isArray(payload) ? payload : [payload]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### view-model.ts
|
||||||
|
```typescript
|
||||||
|
import { useStore } from 'effector-react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
$products,
|
||||||
|
$productsLoading,
|
||||||
|
setProductsFilter,
|
||||||
|
fetchProductsFx
|
||||||
|
} from './model';
|
||||||
|
|
||||||
|
export const useProducts = () => {
|
||||||
|
const products = useStore($products);
|
||||||
|
const isLoading = useStore($productsLoading);
|
||||||
|
|
||||||
|
const updateFilter = useCallback((newFilter) => {
|
||||||
|
setProductsFilter(newFilter);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
products,
|
||||||
|
isLoading,
|
||||||
|
updateFilter
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI Component
|
||||||
|
```typescript
|
||||||
|
import React from 'react';
|
||||||
|
import { useProducts } from '../view-model';
|
||||||
|
import { ProductCard } from './ProductCard';
|
||||||
|
|
||||||
|
export const ProductsList: React.FC = () => {
|
||||||
|
const { products, isLoading } = useProducts();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{products.map(product => (
|
||||||
|
<ProductCard key={product.id} product={product} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
208
docs/authentication.md
Normal file
208
docs/authentication.md
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
# Authentication Documentation
|
||||||
|
|
||||||
|
This document explains the authentication setup for the Merchant Operator App.
|
||||||
|
|
||||||
|
## OIDC Authentication
|
||||||
|
|
||||||
|
The application uses OpenID Connect (OIDC) for authentication with the following features:
|
||||||
|
|
||||||
|
- Dynamic configuration loading from API endpoint `/oidc.json`
|
||||||
|
- Token storage in localStorage via WebStorageStateStore
|
||||||
|
- Automatic token refresh with silent renewal
|
||||||
|
- Refresh token support via offline_access scope
|
||||||
|
- Protected routes with role-based access control
|
||||||
|
- Smart authentication flow with auto-login and manual logout tracking
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
The OIDC configuration is fetched from the API endpoint `/oidc.json`. This endpoint should return a JSON object with the following structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"authorityUrl": "https://your-oidc-provider.com",
|
||||||
|
"clientId": "your-client-id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The application requires the `CLIENT_API_URL` environment variable to be set to the base URL of the API server. The OIDC configuration is then fetched from `${CLIENT_API_URL}/oidc.json` using the OidcService.
|
||||||
|
|
||||||
|
#### OIDC Client Configuration
|
||||||
|
|
||||||
|
The application configures the OIDC client with the following settings:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
authority: config.authorityUrl,
|
||||||
|
client_id: config.clientId,
|
||||||
|
redirect_uri: `${window.location.origin}/auth/callback`,
|
||||||
|
response_type: "code",
|
||||||
|
scope: "openid profile email offline_access", // offline_access for refresh tokens
|
||||||
|
post_logout_redirect_uri: window.location.origin,
|
||||||
|
userStore: new WebStorageStateStore({ store: window.localStorage }),
|
||||||
|
loadUserInfo: true,
|
||||||
|
automaticSilentRenew: true,
|
||||||
|
revokeTokensOnSignout: true,
|
||||||
|
monitorSession: true,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key configuration details:
|
||||||
|
- `offline_access` scope is requested to enable refresh tokens
|
||||||
|
- `automaticSilentRenew` is enabled to automatically refresh tokens in the background
|
||||||
|
- `revokeTokensOnSignout` ensures tokens are properly revoked when logging out
|
||||||
|
- `monitorSession` enables session monitoring to detect changes
|
||||||
|
|
||||||
|
### Authentication Flow
|
||||||
|
|
||||||
|
#### Initial Authentication
|
||||||
|
|
||||||
|
1. The `AuthProvider` component initializes the `UserManager` from `oidc-client-ts` using the configuration fetched from the API endpoint `/oidc.json`.
|
||||||
|
2. It checks if the user is already authenticated by calling `userManager.getUser()`.
|
||||||
|
3. If a user is found but the token is expired or about to expire, it attempts a silent token renewal.
|
||||||
|
4. If no user is found and the user has not manually logged out (tracked in localStorage), the app automatically initiates the login process.
|
||||||
|
5. The login process first tries silent login (`userManager.signinSilent()`).
|
||||||
|
6. If silent login fails, it falls back to redirect login (`userManager.signinRedirect()`).
|
||||||
|
7. After successful authentication at the provider, the user is redirected back to the application's callback URL (`/auth/callback`).
|
||||||
|
8. The `AuthCallback` component handles the redirect, processes the response, and stores the user session.
|
||||||
|
|
||||||
|
#### Token Renewal
|
||||||
|
|
||||||
|
1. The application uses `automaticSilentRenew` to automatically refresh tokens in the background.
|
||||||
|
2. When a token is about to expire, the OIDC client attempts a silent renewal.
|
||||||
|
3. If silent renewal fails, the application will detect this during API calls and can redirect to login if needed.
|
||||||
|
|
||||||
|
#### Manual vs. Auto Login
|
||||||
|
|
||||||
|
The application implements a smart authentication flow that distinguishes between first visits and visits after manual logout:
|
||||||
|
|
||||||
|
1. **First Visit or Regular Visit**: The app automatically initiates the login process.
|
||||||
|
2. **After Manual Logout**: The app requires the user to click the "Sign in" button to log in again.
|
||||||
|
|
||||||
|
This behavior is controlled by a `manuallyLoggedOut` flag that is:
|
||||||
|
- Stored in localStorage to persist across page refreshes and browser sessions
|
||||||
|
- Set to `true` when the user manually logs out
|
||||||
|
- Reset to `false` when the user successfully logs in or manually clicks the login button
|
||||||
|
|
||||||
|
### Protected Routes
|
||||||
|
|
||||||
|
Routes that require authentication can be protected using the `ProtectedRoute` component:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { ProtectedRoute } from '@domains/auth'; // Adjusted path based on current structure
|
||||||
|
|
||||||
|
// Basic protection
|
||||||
|
<ProtectedRoute>
|
||||||
|
<YourComponent />
|
||||||
|
</ProtectedRoute>
|
||||||
|
|
||||||
|
// With role-based protection
|
||||||
|
<ProtectedRoute requiredRoles={['admin']}>
|
||||||
|
<AdminComponent />
|
||||||
|
</ProtectedRoute>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Making Authenticated API Calls
|
||||||
|
|
||||||
|
Use the `useApiService` hook (now located in `src/shared/lib/api/apiService.ts`) to make API calls.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { useApiService } from '@shared/lib/api'; // Use the alias
|
||||||
|
|
||||||
|
function YourComponent() {
|
||||||
|
const api = useApiService();
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
const response = await api.get('/your-endpoint');
|
||||||
|
const data = await response.json();
|
||||||
|
// Process data
|
||||||
|
};
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The current simplified `useApiService` hook:
|
||||||
|
- Checks if the user is authenticated using `useAuth`.
|
||||||
|
- **Does not** automatically add the access token to requests (this would need to be added if required by the backend).
|
||||||
|
- **Does not** handle token refresh or expiration explicitly. It relies on the underlying `oidc-client-ts` library's session management.
|
||||||
|
- Logs an error if a 401 Unauthorized response is received.
|
||||||
|
|
||||||
|
### Logout
|
||||||
|
|
||||||
|
To log a user out, use the `logout` function returned by the `useAuth` hook. This function triggers the `logoutFx` effect, which performs the following steps:
|
||||||
|
|
||||||
|
1. Sets the `manuallyLoggedOut` flag to `true` in localStorage
|
||||||
|
2. Clears the user from the store
|
||||||
|
3. Initiates the OIDC sign-out redirect flow via `userManager.signoutRedirect()`
|
||||||
|
|
||||||
|
The logout process is designed to ensure that the user remains logged out even after page refreshes or browser restarts, until they explicitly choose to log in again.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { useAuth } from '@domains/auth';
|
||||||
|
|
||||||
|
function LogoutButton() {
|
||||||
|
const { logout, user } = useAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="px-4 py-2 bg-[#653cee] hover:bg-[#4e2eb8] text-white rounded-md"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `logoutFx` effect is configured to revoke tokens on signout using the `revokeTokensOnSignout: true` setting in the OIDC client configuration. This ensures that tokens are properly invalidated when logging out.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Authentication configuration error**
|
||||||
|
- Check that the API endpoint `/oidc.json` is accessible
|
||||||
|
- Verify that the API returns the correct JSON structure with `authorityUrl` and `clientId` fields
|
||||||
|
- Ensure the `CLIENT_API_URL` environment variable is correctly set
|
||||||
|
|
||||||
|
2. **Token refresh failures**
|
||||||
|
- Check that the OIDC server is configured to allow refresh tokens
|
||||||
|
- Verify that the `offline_access` scope is included in the OIDC configuration
|
||||||
|
- Check browser console for silent renewal errors
|
||||||
|
|
||||||
|
3. **CORS errors**
|
||||||
|
- Ensure the OIDC server has the correct origins configured
|
||||||
|
- Check that redirect URIs are properly set up
|
||||||
|
- Verify that the OIDC server allows the application's origin
|
||||||
|
|
||||||
|
4. **Auto-login not working**
|
||||||
|
- Check if the `auth_manually_logged_out` flag in localStorage is set to `true`
|
||||||
|
- Clear the flag by setting it to `false` or removing it from localStorage
|
||||||
|
- Verify that the user doesn't have an existing session that's invalid
|
||||||
|
|
||||||
|
5. **Logout not working properly**
|
||||||
|
- Check browser console for errors during the logout process
|
||||||
|
- Verify that the `revokeTokensOnSignout` setting is enabled
|
||||||
|
- Check if the OIDC server is properly configured to handle logout requests
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
The application includes extensive logging to help diagnose authentication issues:
|
||||||
|
|
||||||
|
1. **Check localStorage**: The application stores authentication state in localStorage:
|
||||||
|
- `auth_manually_logged_out`: Tracks if the user has manually logged out
|
||||||
|
- `oidc.user:[authority]:[client_id]`: Contains the user session data
|
||||||
|
|
||||||
|
2. **Console Logging**: The application logs detailed information about the authentication process:
|
||||||
|
- Authentication initialization and configuration
|
||||||
|
- Token validation and renewal attempts
|
||||||
|
- Login and logout operations
|
||||||
|
- State changes in the authentication store
|
||||||
|
|
||||||
|
3. **Enable Debug Mode**: For even more detailed logging, enable debug mode by setting the `CLIENT_DEBUG` environment variable to `true` in your `.env` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
CLIENT_DEBUG=true
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables additional logging in various parts of the application, including more detailed OIDC client logs.
|
120
docs/client-env-vars.md
Normal file
120
docs/client-env-vars.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# Client-Side Environment Variables
|
||||||
|
|
||||||
|
This document explains the client-side environment variables used in the application.
|
||||||
|
|
||||||
|
## Available Variables
|
||||||
|
|
||||||
|
The application uses the following client-side environment variables, all prefixed with `CLIENT_`:
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|---------------------|--------------------------------------------|-------------------------|
|
||||||
|
| `CLIENT_API_URL` | Base URL for backend API requests | `https://localhost:7205`|
|
||||||
|
| `CLIENT_BASE_PATH` | Base path for client-side routing | `/` |
|
||||||
|
| `CLIENT_APP_NAME` | Application name displayed in UI | `Merchant Operator App` |
|
||||||
|
| `CLIENT_DEBUG` | Enable debug mode | `false` |
|
||||||
|
|
||||||
|
## How to Set Variables
|
||||||
|
|
||||||
|
### Development Environment
|
||||||
|
|
||||||
|
For local development, set these variables in:
|
||||||
|
|
||||||
|
1. `.env.development` (shared development settings)
|
||||||
|
2. `.env.development.local` (personal development settings, gitignored)
|
||||||
|
|
||||||
|
Example `.env.development`:
|
||||||
|
```
|
||||||
|
CLIENT_BASE_PATH=/
|
||||||
|
CLIENT_API_URL=https://localhost:7205
|
||||||
|
CLIENT_APP_NAME=Merchant Operator App
|
||||||
|
CLIENT_DEBUG=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Vite automatically makes variables prefixed with `CLIENT_` available via `import.meta.env`. **Remember to restart the Vite dev server after changing `.env` files.**
|
||||||
|
|
||||||
|
### Docker Environment
|
||||||
|
|
||||||
|
For Docker deployments, environment variables are configured **at runtime** through Docker Compose or environment variables:
|
||||||
|
|
||||||
|
#### Using docker-compose.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
merchant-operator-app:
|
||||||
|
environment:
|
||||||
|
- CLIENT_API_URL=https://api.example.com
|
||||||
|
- CLIENT_BASE_PATH=/merchant-app
|
||||||
|
- CLIENT_APP_NAME=Merchant Operator App
|
||||||
|
- CLIENT_DEBUG=false
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using docker-compose.override.yml
|
||||||
|
|
||||||
|
Create a `docker-compose.override.yml` file from the template and set your environment-specific values:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
merchant-operator-app:
|
||||||
|
environment:
|
||||||
|
- CLIENT_API_URL=https://api.example.com
|
||||||
|
- CLIENT_BASE_PATH=/merchant-app
|
||||||
|
- CLIENT_APP_NAME=Merchant Operator App
|
||||||
|
- CLIENT_DEBUG=false
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using .env file with docker-compose
|
||||||
|
|
||||||
|
Create a `.env` file in the same directory as your `docker-compose.yml`:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
# .env
|
||||||
|
CLIENT_API_URL=https://api.example.com
|
||||||
|
CLIENT_BASE_PATH=/merchant-app
|
||||||
|
CLIENT_APP_NAME=Merchant Operator App
|
||||||
|
CLIENT_DEBUG=false
|
||||||
|
# ... other variables
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using Docker run command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -e CLIENT_BASE_PATH=/merchant-app -e CLIENT_API_URL=https://api.example.com -e CLIENT_APP_NAME="Merchant Operator App" -e CLIENT_DEBUG=false your-image-name
|
||||||
|
```
|
||||||
|
|
||||||
|
## How Runtime Environment Variables Work
|
||||||
|
|
||||||
|
The application uses a special mechanism to handle environment variables at runtime in Docker:
|
||||||
|
|
||||||
|
1. When the Docker container starts, the `docker-entrypoint.sh` script runs
|
||||||
|
2. The script extracts all environment variables starting with `CLIENT_` from the container's environment
|
||||||
|
3. These variables are written to a `.env` file in the app's directory
|
||||||
|
4. The Vite app reads these variables at runtime
|
||||||
|
|
||||||
|
This approach allows you to:
|
||||||
|
- Use the same container image in different environments
|
||||||
|
- Change environment variables without rebuilding the image
|
||||||
|
- Pass environment variables through Docker Compose, environment files, or directly via Docker run commands
|
||||||
|
|
||||||
|
## Accessing Variables in Code
|
||||||
|
|
||||||
|
All environment variables are accessed using Vite's `import.meta.env` object:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Direct access
|
||||||
|
const apiUrl = import.meta.env.CLIENT_API_URL;
|
||||||
|
const basePath = import.meta.env.CLIENT_BASE_PATH;
|
||||||
|
const appName = import.meta.env.CLIENT_APP_NAME;
|
||||||
|
const debug = import.meta.env.CLIENT_DEBUG;
|
||||||
|
```
|
||||||
|
|
||||||
|
Utility functions are available to access common values:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { getBasePath, getAssetPath } from '../shared/lib/config';
|
||||||
|
|
||||||
|
// Get the base path
|
||||||
|
const basePath = getBasePath(); // "/merchant-app"
|
||||||
|
|
||||||
|
// Get a path with the base path prepended
|
||||||
|
const logoPath = getAssetPath('/images/logo.png'); // "/merchant-app/images/logo.png"
|
||||||
|
```
|
182
docs/configuration.md
Normal file
182
docs/configuration.md
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
# Merchant Operator App Configuration
|
||||||
|
|
||||||
|
This document describes the configuration options and mechanisms used in the Merchant Operator App.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The application uses environment variables for configuration, which are injected into the application at runtime through `window.appConfig`.
|
||||||
|
|
||||||
|
### Runtime Environment Variables
|
||||||
|
|
||||||
|
Environment variables are loaded at runtime, not build time. This allows for different configurations in different environments without rebuilding the application.
|
||||||
|
|
||||||
|
#### Development Environment
|
||||||
|
|
||||||
|
In development, environment variables are loaded from `.env` files and injected into the application through `window.appConfig` by the Vite plugin.
|
||||||
|
|
||||||
|
#### Production Environment
|
||||||
|
|
||||||
|
In production (Docker), environment variables are loaded from the container environment and injected into the application through `window.appConfig` by the `docker-entrypoint.sh` script.
|
||||||
|
|
||||||
|
### Environment Variable Naming
|
||||||
|
|
||||||
|
- All environment variables used by the client application should start with `CLIENT_`
|
||||||
|
- These variables are converted to camelCase in `window.appConfig`
|
||||||
|
- Example: `CLIENT_API_URL` becomes `window.appConfig.apiUrl`
|
||||||
|
|
||||||
|
### Available Environment Variables
|
||||||
|
|
||||||
|
| Environment Variable | Description | Default Value |
|
||||||
|
|----------------------|-------------|---------------|
|
||||||
|
| `CLIENT_API_URL` | URL of the API server | Required, or can be set to "DETECT" for auto-detection |
|
||||||
|
| `CLIENT_BASE_PATH` | Base path of the application | "/" |
|
||||||
|
| `CLIENT_APP_NAME` | Name of the application | "Merchant Operator App" |
|
||||||
|
| `CLIENT_DEBUG` | Enable debug mode | "false" |
|
||||||
|
| `CLIENT_MAP_TILE_ENDPOINT` | Endpoint for map tiles | "" |
|
||||||
|
| `CLIENT_MAP_TILE_API_KEY` | API key for map tiles | "" |
|
||||||
|
|
||||||
|
## URL Handling
|
||||||
|
|
||||||
|
The application uses the built-in `URL` class for proper URL handling to avoid issues with slashes and ensure consistent URL formatting.
|
||||||
|
|
||||||
|
### URL Utilities
|
||||||
|
|
||||||
|
The application includes utility functions in `src/shared/lib/url-utils.ts` for common URL operations:
|
||||||
|
|
||||||
|
#### `joinUrl(base, ...paths)`
|
||||||
|
|
||||||
|
Safely joins a base URL with path segments.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example
|
||||||
|
const apiUrl = joinUrl('https://api.example.com', 'v1', 'users');
|
||||||
|
// Result: https://api.example.com/v1/users
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `createHashUrl(base, basePath, hashPath)`
|
||||||
|
|
||||||
|
Creates a properly formatted hash URL with the hash fragment after the trailing slash of the base path.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example
|
||||||
|
const url = createHashUrl('https://example.com', 'app', 'users/123');
|
||||||
|
// Result: https://example.com/app/#users/123
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `normalizeUrl(url)`
|
||||||
|
|
||||||
|
Normalizes a URL by ensuring it has a trailing slash.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example
|
||||||
|
const normalizedUrl = normalizeUrl('https://api.example.com/v1');
|
||||||
|
// Result: https://api.example.com/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `cleanupUrl()`
|
||||||
|
|
||||||
|
Cleans up the current URL by removing query parameters and hash fragments.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example: Current URL is https://example.com/app/?code=123#/callback
|
||||||
|
cleanupUrl();
|
||||||
|
// Result: https://example.com/app/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hash-Based Routing
|
||||||
|
|
||||||
|
The application uses hash-based routing (`/#`) instead of URL-based routing for better compatibility with static hosting and to avoid server configuration for deep linking.
|
||||||
|
|
||||||
|
- All routes are prefixed with `/#`
|
||||||
|
- The hash fragment always comes after the trailing slash of the base path
|
||||||
|
- Example: `https://example.com/app/#users` (correct) vs `https://example.com/app#users` (incorrect)
|
||||||
|
|
||||||
|
## OIDC Authentication
|
||||||
|
|
||||||
|
The application uses OIDC for authentication with the following configuration:
|
||||||
|
|
||||||
|
### OIDC Configuration
|
||||||
|
|
||||||
|
- The OIDC configuration is fetched from the API server at `/apiendpoint/oidc.json`
|
||||||
|
- The OIDC provider is separate from the API provider
|
||||||
|
- The API provider gives information about the OIDC provider location
|
||||||
|
|
||||||
|
### OIDC URLs
|
||||||
|
|
||||||
|
- Authority URL: The URL of the OIDC provider, normalized to ensure it has a trailing slash
|
||||||
|
- Redirect URI: The URL to redirect to after authentication, using hash-based routing
|
||||||
|
- Post-Logout Redirect URI: The URL to redirect to after logout, using hash-based routing
|
||||||
|
|
||||||
|
### OIDC Callback Handling
|
||||||
|
|
||||||
|
- After authentication, the callback URL is cleaned up to remove query parameters and hash fragments
|
||||||
|
- The user is redirected to the intended page or the home page
|
||||||
|
|
||||||
|
## API URL Configuration
|
||||||
|
|
||||||
|
The API URL can be configured in several ways:
|
||||||
|
|
||||||
|
### Static Configuration
|
||||||
|
|
||||||
|
Set the `CLIENT_API_URL` environment variable to the URL of the API server.
|
||||||
|
|
||||||
|
### Auto-Detection
|
||||||
|
|
||||||
|
Set the `CLIENT_API_URL` environment variable to `"DETECT"` to automatically detect the API URL based on the current window location.
|
||||||
|
|
||||||
|
The auto-detection logic:
|
||||||
|
1. Takes the current window location (e.g., `https://www.example.com/path1/path2/path3`)
|
||||||
|
2. Extracts the first path segment (e.g., `path1`)
|
||||||
|
3. Constructs a new URL with just the origin and first path segment (e.g., `https://www.example.com/path1/`)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `https://www.example.com/path1/path2/path3/` → `https://www.example.com/path1/`
|
||||||
|
- `https://www.example.com/` → `https://www.example.com/`
|
||||||
|
- `https://www.example.com/path1` → `https://www.example.com/path1/`
|
||||||
|
|
||||||
|
If auto-detection fails, a fatal error is logged and the application does not proceed.
|
||||||
|
|
||||||
|
## Accessing Configuration in Code
|
||||||
|
|
||||||
|
### API URL
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Use OpenAPI.BASE for the normalized API URL
|
||||||
|
import { OpenAPI } from "@merchant-api/core/OpenAPI";
|
||||||
|
|
||||||
|
const apiUrl = OpenAPI.BASE;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Access environment variables through window.appConfig
|
||||||
|
const appName = window.appConfig.appName;
|
||||||
|
const debug = window.appConfig.debug;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Scripts
|
||||||
|
|
||||||
|
### Generate App Config
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run generate-app-config
|
||||||
|
```
|
||||||
|
|
||||||
|
Generates the `app-config.js` file in the `public` directory based on environment variables.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Builds the application for production.
|
||||||
|
|
||||||
|
### Development Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Starts the development server with hot reloading.
|
98
docs/deployment.md
Normal file
98
docs/deployment.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# Path-Agnostic SPA Deployment Guide
|
||||||
|
|
||||||
|
This project is configured for path-agnostic deployment, allowing the application to be served from any base path without requiring a rebuild.
|
||||||
|
|
||||||
|
## Configuration Files
|
||||||
|
|
||||||
|
- **docker-compose.yml**: Main Docker Compose configuration with variables
|
||||||
|
- **docker-compose.override.yml.template**: Template for environment-specific overrides
|
||||||
|
- **.env.template**: Template for environment variables
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
1. Copy the template files to create your environment-specific configurations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp docker-compose.override.yml.template docker-compose.override.yml
|
||||||
|
cp .env.template .env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Edit the `.env` and/or `docker-compose.override.yml` files to set your environment-specific values:
|
||||||
|
|
||||||
|
### Key Configuration Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|---------------------|--------------------------------------------|-------------------------|
|
||||||
|
| CLIENT_BASE_PATH | Base path where the app will be served | `/` |
|
||||||
|
| CLIENT_API_URL | Base URL for backend API | `https://localhost:7205`|
|
||||||
|
| CLIENT_APP_NAME | Application name displayed in UI | `Merchant Operator App` |
|
||||||
|
| CLIENT_DEBUG | Enable debug mode | `false` |
|
||||||
|
| ENABLE_TLS | Whether to enable TLS | `false` |
|
||||||
|
| TRAEFIK_ENTRYPOINT | Traefik entrypoint to use | `web` |
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
For local development, you can use the default values or customize as needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Deployment
|
||||||
|
|
||||||
|
For production, build the image and then deploy with environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the image
|
||||||
|
docker build -t merchant-operator-app .
|
||||||
|
|
||||||
|
# Deploy with environment variables
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment variables are passed at runtime through docker-compose.yml or docker-compose.override.yml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
merchant-operator-app:
|
||||||
|
environment:
|
||||||
|
- CLIENT_BASE_PATH=/app
|
||||||
|
- CLIENT_API_URL=https://api.example.com
|
||||||
|
- CLIENT_APP_NAME=Merchant Operator App
|
||||||
|
- CLIENT_DEBUG=false
|
||||||
|
- CLIENT_OIDC_AUTHORITY=https://auth.example.com
|
||||||
|
- CLIENT_OIDC_CLIENT_ID=your-client-id
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Removed client-side env vars section; see docs/client-env-vars.md for details -->
|
||||||
|
|
||||||
|
Variable names are converted from `CLIENT_SNAKE_CASE` to `camelCase` automatically.
|
||||||
|
|
||||||
|
## Using Base Path in the Application
|
||||||
|
|
||||||
|
The application is configured to use the base path from the configuration. In your React components, use the utility functions from `src/utils/config.js`:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { getAssetPath } from '../utils/config';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
return <img src={getAssetPath('/images/logo.png')} alt="Logo" />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. During development, Vite makes environment variables prefixed with `CLIENT_` available via `import.meta.env`.
|
||||||
|
2. The application is built without any environment-specific configuration.
|
||||||
|
3. At container startup, the `docker-entrypoint.sh` script extracts all environment variables starting with `CLIENT_` and writes them to a `.env` file in the app's directory.
|
||||||
|
4. The Vite application reads these variables at runtime.
|
||||||
|
5. The Nginx configuration is set up to handle SPA routing and asset caching.
|
||||||
|
6. The `getBasePath()` and `getAssetPath()` utility functions provide the correct paths in your application code.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- If assets are not loading, check that you're using the `getAssetPath()` function for all asset URLs.
|
||||||
|
- If routing is not working, ensure that your router is configured to use the base path.
|
||||||
|
- Check Nginx logs for any errors: `docker-compose logs Merchant-operator-app`
|
212
docs/effector-guide.md
Normal file
212
docs/effector-guide.md
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
# Effector Guide
|
||||||
|
|
||||||
|
This guide provides an overview of how to use Effector in our application, following our Flux-style architecture.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
Effector is a state management library that implements the Flux pattern with three main primitives:
|
||||||
|
|
||||||
|
1. **Events**: Trigger state changes
|
||||||
|
2. **Stores**: Hold application state
|
||||||
|
3. **Effects**: Handle side effects (async operations)
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Creating Events
|
||||||
|
|
||||||
|
Events are functions that can be called to trigger state changes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createEvent } from 'effector';
|
||||||
|
|
||||||
|
// Create an event
|
||||||
|
export const increment = createEvent();
|
||||||
|
export const addTodo = createEvent<string>();
|
||||||
|
export const updateUser = createEvent<{ name: string, email: string }>();
|
||||||
|
|
||||||
|
// Call the event
|
||||||
|
increment();
|
||||||
|
addTodo('Buy milk');
|
||||||
|
updateUser({ name: 'John', email: 'john@example.com' });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Stores
|
||||||
|
|
||||||
|
Stores hold the application state and update in response to events:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createStore } from 'effector';
|
||||||
|
import { increment, addTodo } from './events';
|
||||||
|
|
||||||
|
// Create a store with initial state
|
||||||
|
export const $counter = createStore(0)
|
||||||
|
.on(increment, state => state + 1);
|
||||||
|
|
||||||
|
export const $todos = createStore<string[]>([])
|
||||||
|
.on(addTodo, (state, todo) => [...state, todo]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Effects
|
||||||
|
|
||||||
|
Effects handle asynchronous operations:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createEffect } from 'effector';
|
||||||
|
import { api } from '@shared/lib/api';
|
||||||
|
|
||||||
|
// Create an effect
|
||||||
|
export const fetchUserFx = createEffect(async (userId: string) => {
|
||||||
|
const response = await api.getUser(userId);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call the effect
|
||||||
|
fetchUserFx('123');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handling Effect States
|
||||||
|
|
||||||
|
Effects have three states: pending, done, and fail:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createStore } from 'effector';
|
||||||
|
import { fetchUserFx } from './effects';
|
||||||
|
|
||||||
|
// Create stores for different effect states
|
||||||
|
export const $isLoading = createStore(false)
|
||||||
|
.on(fetchUserFx.pending, (_, isPending) => isPending);
|
||||||
|
|
||||||
|
export const $user = createStore(null)
|
||||||
|
.on(fetchUserFx.doneData, (_, user) => user);
|
||||||
|
|
||||||
|
export const $error = createStore(null)
|
||||||
|
.on(fetchUserFx.failData, (_, error) => error)
|
||||||
|
.on(fetchUserFx, () => null); // Reset error when effect is called
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using with React
|
||||||
|
|
||||||
|
### useStore Hook
|
||||||
|
|
||||||
|
The `useStore` hook connects Effector stores to React components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useStore } from 'effector-react';
|
||||||
|
import { $counter, increment } from './model';
|
||||||
|
|
||||||
|
const Counter = () => {
|
||||||
|
const count = useStore($counter);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Count: {count}</p>
|
||||||
|
<button onClick={() => increment()}>Increment</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### useEvent Hook
|
||||||
|
|
||||||
|
The `useEvent` hook creates a stable callback for events:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useStore, useEvent } from 'effector-react';
|
||||||
|
import { $todos, addTodo } from './model';
|
||||||
|
|
||||||
|
const TodoList = () => {
|
||||||
|
const todos = useStore($todos);
|
||||||
|
const handleAddTodo = useEvent(addTodo);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={() => handleAddTodo('New todo')}>Add Todo</button>
|
||||||
|
<ul>
|
||||||
|
{todos.map((todo, index) => (
|
||||||
|
<li key={index}>{todo}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Patterns
|
||||||
|
|
||||||
|
### Derived Stores (Computed Values)
|
||||||
|
|
||||||
|
Create derived stores using the `.map` method:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createStore } from 'effector';
|
||||||
|
|
||||||
|
export const $todos = createStore([
|
||||||
|
{ id: 1, text: 'Buy milk', completed: false },
|
||||||
|
{ id: 2, text: 'Clean house', completed: true }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Derived store for completed todos
|
||||||
|
export const $completedTodos = $todos.map(
|
||||||
|
todos => todos.filter(todo => todo.completed)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Derived store for todo count
|
||||||
|
export const $todoCount = $todos.map(todos => todos.length);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combining Stores
|
||||||
|
|
||||||
|
Combine multiple stores into one:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createStore, combine } from 'effector';
|
||||||
|
|
||||||
|
export const $user = createStore({ name: 'John' });
|
||||||
|
export const $settings = createStore({ theme: 'dark' });
|
||||||
|
|
||||||
|
// Combined store
|
||||||
|
export const $appState = combine({
|
||||||
|
user: $user,
|
||||||
|
settings: $settings
|
||||||
|
});
|
||||||
|
|
||||||
|
// Or with a custom mapper function
|
||||||
|
export const $userData = combine(
|
||||||
|
$user,
|
||||||
|
$settings,
|
||||||
|
(user, settings) => ({
|
||||||
|
...user,
|
||||||
|
theme: settings.theme
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset Events
|
||||||
|
|
||||||
|
Reset stores to their initial state:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createEvent, createStore } from 'effector';
|
||||||
|
|
||||||
|
export const reset = createEvent();
|
||||||
|
|
||||||
|
export const $counter = createStore(0)
|
||||||
|
.reset(reset); // Reset to initial state (0)
|
||||||
|
|
||||||
|
// Call reset to restore initial state
|
||||||
|
reset();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Keep model.ts pure**: Avoid side effects in store updates.
|
||||||
|
|
||||||
|
2. **Use TypeScript**: Define types for all events, stores, and effects.
|
||||||
|
|
||||||
|
3. **Organize by domain**: Group related events, stores, and effects in domain folders.
|
||||||
|
|
||||||
|
4. **Use view-model.ts**: Create hooks that encapsulate Effector logic for React components.
|
||||||
|
|
||||||
|
5. **Keep UI components simple**: UI components should only consume data from hooks and call events.
|
||||||
|
|
||||||
|
6. **Test model.ts**: Write unit tests for your Effector logic.
|
64
docs/flux-vs-mvvm.md
Normal file
64
docs/flux-vs-mvvm.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Flux vs. MVVM Architecture
|
||||||
|
|
||||||
|
## Decision Matrix: Flux vs. MVVM
|
||||||
|
|
||||||
|
| Criterion | Plain Effector (Flux) | MVVM Layer on Top |
|
||||||
|
|-----------|----------------------|-------------------|
|
||||||
|
| Boilerplate | Low | Higher |
|
||||||
|
| React Ecosystem Fit | Native | Non-idiomatic |
|
||||||
|
| Two-way Binding Risk | None | Possible |
|
||||||
|
| Testability | High (domain files are pure) | High but more layers |
|
||||||
|
| Refactor Complexity | Lower | Higher |
|
||||||
|
|
||||||
|
## Why We Chose Flux with Effector
|
||||||
|
|
||||||
|
For our React SPA, we've adopted an "Effector-based Flux architecture" for the following reasons:
|
||||||
|
|
||||||
|
1. **Better React Integration**: Flux's unidirectional data flow aligns perfectly with React's rendering model.
|
||||||
|
|
||||||
|
2. **Simplicity**: Fewer abstractions and less boilerplate compared to MVVM.
|
||||||
|
|
||||||
|
3. **Predictability**: One-way data flow makes it easier to trace how state changes propagate through the application.
|
||||||
|
|
||||||
|
4. **Testability**: Pure functions in model.ts are easy to test without complex mocking.
|
||||||
|
|
||||||
|
5. **TypeScript Friendly**: Effector has excellent TypeScript support, providing type safety throughout the application.
|
||||||
|
|
||||||
|
## Flux Architecture Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
|
│ Actions │────>│ Store │────>│ View │
|
||||||
|
│ (Events) │ │ (State) │ │ (React) │
|
||||||
|
└─────────────┘ └─────────────┘ └─────────────┘
|
||||||
|
▲ │
|
||||||
|
│ │
|
||||||
|
└───────────────────────────────────────┘
|
||||||
|
User Interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
## Effector Implementation
|
||||||
|
|
||||||
|
Effector provides three main primitives that map to Flux concepts:
|
||||||
|
|
||||||
|
1. **Events** (Actions): Trigger state changes
|
||||||
|
```typescript
|
||||||
|
export const setProductName = createEvent<string>();
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Stores** (State): Hold application state
|
||||||
|
```typescript
|
||||||
|
export const $product = createStore(initialState)
|
||||||
|
.on(setProductName, (state, name) => ({ ...state, name }));
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Effects** (Async Actions): Handle side effects
|
||||||
|
```typescript
|
||||||
|
export const fetchProductFx = createEffect(async (id: string) => {
|
||||||
|
return await api.getProduct(id);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
While MVVM is a valid architecture pattern, Flux with Effector provides a more natural fit for React applications, with less boilerplate and better alignment with React's unidirectional data flow model. This approach keeps our codebase clean, testable, and maintainable.
|
121
docs/local-development.md
Normal file
121
docs/local-development.md
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
# Local Development Guide
|
||||||
|
|
||||||
|
This guide explains how to run the application locally with different base path configurations.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The application uses environment variables to configure settings during development. These variables can be set in several ways:
|
||||||
|
|
||||||
|
1. In `.env.development` (shared development settings)
|
||||||
|
2. In `.env.development.local` (personal development settings, gitignored)
|
||||||
|
3. Via command line when running scripts
|
||||||
|
|
||||||
|
### Client-Side Environment Variables
|
||||||
|
|
||||||
|
Variables with the `CLIENT_` prefix will be available in the browser through the `window.appConfig` object. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
CLIENT_API_URL=https://localhost:7205
|
||||||
|
CLIENT_APP_NAME=My App
|
||||||
|
CLIENT_DEBUG=true
|
||||||
|
```
|
||||||
|
|
||||||
|
These will be available in the browser as:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
window.appConfig.apiUrl // "https://localhost:7205"
|
||||||
|
window.appConfig.appName // "My App"
|
||||||
|
window.appConfig.debug // "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
Variable names are converted from `CLIENT_SNAKE_CASE` to `camelCase` automatically.
|
||||||
|
|
||||||
|
## Setting Up Local Development
|
||||||
|
|
||||||
|
### Basic Setup
|
||||||
|
|
||||||
|
1. Copy the template file to create your local environment configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.development.local.template .env.development.local
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Edit `.env.development.local` to set your preferred base path:
|
||||||
|
|
||||||
|
```
|
||||||
|
BASE_PATH=/your-path
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Development Server
|
||||||
|
|
||||||
|
#### Default Development (Root Path)
|
||||||
|
|
||||||
|
To run the development server with the default configuration (usually root path `/`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom Path Development
|
||||||
|
|
||||||
|
To run the development server with a predefined custom path (`/Merchant-app`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev:path
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom Path via Command Line
|
||||||
|
|
||||||
|
To run the development server with any custom path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BASE_PATH=/custom-path npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building for Different Paths
|
||||||
|
|
||||||
|
### Default Build
|
||||||
|
|
||||||
|
To build the application with the default configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Path Build
|
||||||
|
|
||||||
|
To build with a predefined custom path (`/Merchant-app`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:path
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Path via Command Line
|
||||||
|
|
||||||
|
To build with any custom path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BASE_PATH=/custom-path npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. The Vite configuration reads the `BASE_PATH` environment variable
|
||||||
|
2. It dynamically updates the `config.js` file with the appropriate base path during development
|
||||||
|
3. The application uses this configuration to construct URLs correctly
|
||||||
|
4. In production, the `docker-entrypoint.sh` script replaces a placeholder in `config.js` with the actual base path
|
||||||
|
|
||||||
|
## Testing Different Paths
|
||||||
|
|
||||||
|
To test how the application behaves with different base paths:
|
||||||
|
|
||||||
|
1. Set a custom base path in `.env.development.local` or via command line
|
||||||
|
2. Run the development server
|
||||||
|
3. Verify that all links and assets load correctly
|
||||||
|
4. Test navigation within the application
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- If assets are not loading, check that you're using the `getAssetPath()` function for all asset URLs
|
||||||
|
- If the base path is not being applied, make sure the environment variable is being set correctly
|
||||||
|
- Check the console for any error messages related to path configuration
|
140
docs/project-structure.md
Normal file
140
docs/project-structure.md
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# Project Structure
|
||||||
|
|
||||||
|
This document outlines the structure of our application, which follows a domain-driven, Flux-style architecture using Effector for state management.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
app/ # Application entry point and configuration
|
||||||
|
root.tsx # Root component
|
||||||
|
routes.tsx # Route definitions
|
||||||
|
domains/ # Feature domains
|
||||||
|
<feature>/ # e.g., products, auth, orders
|
||||||
|
model.ts # Effector events, stores, effects
|
||||||
|
view-model.ts # React hooks for connecting to the model
|
||||||
|
ui/ # React components
|
||||||
|
index.ts # Exports all components
|
||||||
|
Component.tsx # Individual components
|
||||||
|
shared/ # Shared code
|
||||||
|
lib/ # Utilities, helpers, adapters
|
||||||
|
ui/ # Reusable UI components
|
||||||
|
tests/ # Test files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Domain Structure
|
||||||
|
|
||||||
|
Each domain follows a consistent structure:
|
||||||
|
|
||||||
|
### model.ts
|
||||||
|
|
||||||
|
Contains all Effector-related code:
|
||||||
|
- Events (actions)
|
||||||
|
- Stores (state)
|
||||||
|
- Effects (async operations)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```typescript
|
||||||
|
// domains/products/model.ts
|
||||||
|
import { createStore, createEvent, createEffect } from 'effector';
|
||||||
|
import { ProductsViewsService } from '@lib/api/merchant/services/ProductsViewsService';
|
||||||
|
import type { productCompositeDto } from '@lib/api/merchant/models/productCompositeDto';
|
||||||
|
|
||||||
|
// Events
|
||||||
|
export const setProductsFilter = createEvent<Partial<productQueryRequestDto>>();
|
||||||
|
|
||||||
|
// Effects
|
||||||
|
export const fetchProductsFx = createEffect(async (filter: productQueryRequestDto) => {
|
||||||
|
return await ProductsViewsService.postApiProductCompositeQueryByCanonical({
|
||||||
|
requestBody: filter
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stores
|
||||||
|
export const $products = createStore<productCompositeDto[]>([])
|
||||||
|
.on(fetchProductsFx.doneData, (_, payload) => Array.isArray(payload) ? payload : [payload]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### view-model.ts
|
||||||
|
|
||||||
|
Contains React hooks that connect to the Effector model:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// domains/products/view-model.ts
|
||||||
|
import { useStore } from 'effector-react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
$products,
|
||||||
|
$productsLoading,
|
||||||
|
setProductsFilter,
|
||||||
|
fetchProductsFx
|
||||||
|
} from './model';
|
||||||
|
|
||||||
|
export const useProducts = () => {
|
||||||
|
const products = useStore($products);
|
||||||
|
const isLoading = useStore($productsLoading);
|
||||||
|
|
||||||
|
const updateFilter = useCallback((newFilter) => {
|
||||||
|
setProductsFilter(newFilter);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
products,
|
||||||
|
isLoading,
|
||||||
|
updateFilter
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### ui/
|
||||||
|
|
||||||
|
Contains React components that use the view-model:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// domains/products/ui/ProductsList.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { useProducts } from '../view-model';
|
||||||
|
import { ProductCard } from './ProductCard';
|
||||||
|
|
||||||
|
export const ProductsList: React.FC = () => {
|
||||||
|
const { products, isLoading } = useProducts();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{products.map(product => (
|
||||||
|
<ProductCard key={product.id} product={product} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shared Code
|
||||||
|
|
||||||
|
### shared/lib/
|
||||||
|
|
||||||
|
Contains utilities, helpers, and adapters that are used across multiple domains.
|
||||||
|
|
||||||
|
### shared/ui/
|
||||||
|
|
||||||
|
Contains reusable UI components that are not tied to any specific domain.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Tests are organized alongside the code they test, following the same structure.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Keep domains isolated**: Each domain should be self-contained and not depend on other domains.
|
||||||
|
|
||||||
|
2. **Use named exports**: Avoid default exports to make refactoring easier.
|
||||||
|
|
||||||
|
3. **Keep files small**: Each file should have a single responsibility.
|
||||||
|
|
||||||
|
4. **Use TypeScript**: All files should be written in TypeScript for type safety.
|
||||||
|
|
||||||
|
5. **Follow Flux pattern**: Maintain unidirectional data flow from events to stores to views.
|
246
docs/typescript-conventions.md
Normal file
246
docs/typescript-conventions.md
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
# TypeScript Conventions
|
||||||
|
|
||||||
|
This document outlines our TypeScript conventions for the project.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
We use strict TypeScript settings to ensure type safety:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
### Files and Directories
|
||||||
|
|
||||||
|
- Use kebab-case for file and directory names: `product-list.tsx`, `view-model.ts`
|
||||||
|
- Exception: React components can use PascalCase: `ProductCard.tsx`
|
||||||
|
|
||||||
|
### Variables and Functions
|
||||||
|
|
||||||
|
- Use camelCase for variables and functions: `productList`, `fetchProducts`
|
||||||
|
- Use PascalCase for React components and types: `ProductList`, `ProductDto`
|
||||||
|
- Use ALL_CAPS for constants: `MAX_ITEMS`, `API_URL`
|
||||||
|
|
||||||
|
### Effector Naming
|
||||||
|
|
||||||
|
- Prefix store names with `$`: `$products`, `$isLoading`
|
||||||
|
- Suffix effect names with `Fx`: `fetchProductsFx`, `saveUserFx`
|
||||||
|
- Use descriptive names for events: `setProductFilter`, `resetForm`
|
||||||
|
|
||||||
|
## Type Definitions
|
||||||
|
|
||||||
|
### Interfaces vs. Types
|
||||||
|
|
||||||
|
- Use `interface` for object shapes that might be extended
|
||||||
|
- Use `type` for unions, intersections, and simple object shapes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Interface for extendable objects
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AdminUser extends User {
|
||||||
|
permissions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type for unions and simple objects
|
||||||
|
type Status = 'idle' | 'loading' | 'success' | 'error';
|
||||||
|
|
||||||
|
type ProductFilter = {
|
||||||
|
category?: string;
|
||||||
|
minPrice?: number;
|
||||||
|
maxPrice?: number;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Models
|
||||||
|
|
||||||
|
- Use the generated types from the API client
|
||||||
|
- Create wrapper types when needed for additional properties
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { productCompositeDto } from '@lib/api/merchant/models/productCompositeDto';
|
||||||
|
|
||||||
|
// Extended type with UI-specific properties
|
||||||
|
type ProductWithUIState = productCompositeDto & {
|
||||||
|
isSelected: boolean;
|
||||||
|
isExpanded: boolean;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## React Component Types
|
||||||
|
|
||||||
|
### Functional Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button: React.FC<ButtonProps> = ({
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
className="btn"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Handlers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Form event
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
// Input change
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
// Click event
|
||||||
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Effector Types
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createEvent } from 'effector';
|
||||||
|
|
||||||
|
// Event with no payload
|
||||||
|
export const resetFilter = createEvent();
|
||||||
|
|
||||||
|
// Event with payload
|
||||||
|
export const setSearchQuery = createEvent<string>();
|
||||||
|
|
||||||
|
// Event with complex payload
|
||||||
|
export const updateFilter = createEvent<{
|
||||||
|
category?: string;
|
||||||
|
minPrice?: number;
|
||||||
|
maxPrice?: number;
|
||||||
|
}>();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stores
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createStore } from 'effector';
|
||||||
|
|
||||||
|
// Define the store state type
|
||||||
|
interface FilterState {
|
||||||
|
searchQuery: string;
|
||||||
|
category: string | null;
|
||||||
|
minPrice: number | null;
|
||||||
|
maxPrice: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
const initialState: FilterState = {
|
||||||
|
searchQuery: '',
|
||||||
|
category: null,
|
||||||
|
minPrice: null,
|
||||||
|
maxPrice: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the store
|
||||||
|
export const $filter = createStore<FilterState>(initialState);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Effects
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createEffect } from 'effector';
|
||||||
|
import { ProductsViewsService } from '@lib/api/merchant/services/ProductsViewsService';
|
||||||
|
import type { productQueryRequestDto } from '@lib/api/merchant/models/productQueryRequestDto';
|
||||||
|
import type { productCompositeDto } from '@lib/api/merchant/models/productCompositeDto';
|
||||||
|
|
||||||
|
// Define effect with explicit parameter and return types
|
||||||
|
export const fetchProductsFx = createEffect<
|
||||||
|
productQueryRequestDto,
|
||||||
|
productCompositeDto[],
|
||||||
|
Error
|
||||||
|
>(async (filter) => {
|
||||||
|
const response = await ProductsViewsService.postApiProductCompositeQueryByCanonical({
|
||||||
|
requestBody: filter
|
||||||
|
});
|
||||||
|
return Array.isArray(response) ? response : [response];
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Avoid `any`**: Never use the `any` type. Use `unknown` if the type is truly unknown.
|
||||||
|
|
||||||
|
2. **Use type inference**: Let TypeScript infer types when possible, but add explicit types for function parameters and return values.
|
||||||
|
|
||||||
|
3. **Null vs. undefined**: Use `undefined` for optional values, `null` for intentionally absent values.
|
||||||
|
|
||||||
|
4. **Non-null assertion**: Avoid using the non-null assertion operator (`!`) when possible. Use optional chaining (`?.`) and nullish coalescing (`??`) instead.
|
||||||
|
|
||||||
|
5. **Type guards**: Use type guards to narrow types:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function isProduct(item: unknown): item is productCompositeDto {
|
||||||
|
return (
|
||||||
|
typeof item === 'object' &&
|
||||||
|
item !== null &&
|
||||||
|
'id' in item
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Readonly**: Use `readonly` for immutable properties:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Config {
|
||||||
|
readonly apiUrl: string;
|
||||||
|
readonly timeout: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Generics**: Use generics for reusable components and functions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function fetchData<T>(url: string): Promise<T> {
|
||||||
|
return fetch(url).then(res => res.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fetchData<User[]>('/api/users');
|
||||||
|
```
|
63
docs/utils.md
Normal file
63
docs/utils.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Utility Functions
|
||||||
|
|
||||||
|
## Configuration Utilities
|
||||||
|
|
||||||
|
The application includes utility functions for working with the base path configuration. These functions are located in `src/utils/config.js`.
|
||||||
|
|
||||||
|
### getBasePath()
|
||||||
|
|
||||||
|
Returns the base path from the application configuration.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { getBasePath } from '../utils/config';
|
||||||
|
|
||||||
|
// Example usage
|
||||||
|
const basePath = getBasePath(); // Returns the base path, e.g., '/Merchant-app'
|
||||||
|
```
|
||||||
|
|
||||||
|
### getAssetPath(path)
|
||||||
|
|
||||||
|
Constructs a complete path for an asset by prepending the base path.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { getAssetPath } from '../utils/config';
|
||||||
|
|
||||||
|
// Example usage
|
||||||
|
const logoPath = getAssetPath('/images/logo.png');
|
||||||
|
// If base path is '/Merchant-app', returns '/Merchant-app/images/logo.png'
|
||||||
|
|
||||||
|
// Works with paths with or without leading slash
|
||||||
|
const cssPath = getAssetPath('styles/main.css');
|
||||||
|
// Returns '/Merchant-app/styles/main.css'
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use These Functions
|
||||||
|
|
||||||
|
Use these utility functions whenever you need to reference:
|
||||||
|
|
||||||
|
- Static assets (images, CSS, JavaScript)
|
||||||
|
- API endpoints
|
||||||
|
- Navigation links
|
||||||
|
- Any URL that should be relative to the application's base path
|
||||||
|
|
||||||
|
## Example in a React Component
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React from 'react';
|
||||||
|
import { getAssetPath } from '../utils/config';
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<header>
|
||||||
|
<img src={getAssetPath('/logo.svg')} alt="Company Logo" />
|
||||||
|
<nav>
|
||||||
|
<a href={getAssetPath('/')}>Home</a>
|
||||||
|
<a href={getAssetPath('/about')}>About</a>
|
||||||
|
<a href={getAssetPath('/contact')}>Contact</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
||||||
|
```
|
47
eslint.config.mjs
Normal file
47
eslint.config.mjs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [
|
||||||
|
// 1. Ignore the directories you don't want to lint.
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"./src/lib/api/core/**",
|
||||||
|
"./src/lib/api/services/**",
|
||||||
|
"./src/lib/api/models/**",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// 2. Extend Next.js configs (converted to flat config by `FlatCompat`).
|
||||||
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
|
|
||||||
|
// 3. Any plugin or custom rules config you need.
|
||||||
|
...compat.config({
|
||||||
|
plugins: ["eslint-comments"],
|
||||||
|
rules: {
|
||||||
|
"eslint-comments/no-unused-disable": "off",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 4. Additional custom rules.
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"no-console": ["error", { allow: ["error"] }],
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-function-type": "off",
|
||||||
|
"@typescript-eslint/no-require-imports": "off",
|
||||||
|
"prefer-const": "off",
|
||||||
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
|
"react-hooks/rules-of-hooks": "warn",
|
||||||
|
"@next/next/no-img-element": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
22
index.html
Normal file
22
index.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<script>
|
||||||
|
if (location.pathname !== '/' && !location.pathname.endsWith('/'))
|
||||||
|
location.replace(
|
||||||
|
location.href.replace(/([^\/?#])(?=[?#]|$)/, '$1/')
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Merchant Operator App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
123
memory-bank/README.md
Normal file
123
memory-bank/README.md
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# Memory Bank
|
||||||
|
|
||||||
|
I am an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.
|
||||||
|
|
||||||
|
## Quick Reference (Project Overview)
|
||||||
|
|
||||||
|
## Agent Guidelines
|
||||||
|
|
||||||
|
- **Architecture**: Use Flux-style with Effector for state management
|
||||||
|
- **File Structure**: Follow domain-driven organization (model.ts, view-model.ts, ui/)
|
||||||
|
- **TypeScript**: Use strict mode with proper typing
|
||||||
|
- **Components**: Keep UI components pure and presentational
|
||||||
|
- **State Management**: Use Effector events, stores, and effects
|
||||||
|
- **API Models**: Use the generated DTOs from the API client
|
||||||
|
- **Naming**: Use $ prefix for stores, Fx suffix for effects
|
||||||
|
- **Exports**: Use named exports only, avoid default exports
|
||||||
|
|
||||||
|
### Project Summary
|
||||||
|
- **Name**: Merchant Operator Web Application
|
||||||
|
- **Type**: Client-side only React application for Merchant management
|
||||||
|
- **Core Features**: Menu management, order tracking, Merchant operations
|
||||||
|
- **Tech Stack**: React, TypeScript, Tailwind CSS, Effector, Vite
|
||||||
|
- **Deployment**: Static site deployment
|
||||||
|
- **Current Status**: In development
|
||||||
|
|
||||||
|
### Important Workflow Preferences
|
||||||
|
- **Git Commits**: NEVER commit without asking the user first
|
||||||
|
- **Completion Reminders**: Remind/ask the user when big sections have been done and tested
|
||||||
|
- **Command Line**: Use PowerShell for all command-line operations (Windows environment)
|
||||||
|
- **Development Server**: NEVER run the server unless specifically asked, as user often runs it in the background
|
||||||
|
|
||||||
|
### Key Files Summary
|
||||||
|
1. **projectbrief.md**: Merchant management app with menu editing, order tracking, and Merchant operations
|
||||||
|
2. **systemPatterns.md**: Client-side SPA with React, Effector for state management, component-based architecture
|
||||||
|
3. **techContext.md**: React, TypeScript, Tailwind CSS, Effector, Vite, Jest, ESLint, Prettier, Husky
|
||||||
|
|
||||||
|
### Documentation Sources
|
||||||
|
- **AI Agent Documentation** (`/docs`): Concise, up-to-date docs designed for AI agents
|
||||||
|
|
||||||
|
## Memory Bank Structure
|
||||||
|
|
||||||
|
The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
|
||||||
|
|
||||||
|
flowchart TD
|
||||||
|
PB[projectbrief.md] --> SP[systemPatterns.md]
|
||||||
|
PB --> TC[techContext.md]
|
||||||
|
|
||||||
|
|
||||||
|
### Core Files (Required)
|
||||||
|
1. `projectbrief.md`
|
||||||
|
- Foundation document that shapes all other files
|
||||||
|
- Created at project start if it doesn't exist
|
||||||
|
- Defines core requirements and goals
|
||||||
|
- Source of truth for project scope
|
||||||
|
|
||||||
|
2. `systemPatterns.md`
|
||||||
|
- System architecture
|
||||||
|
- Key technical decisions
|
||||||
|
- Design patterns in use
|
||||||
|
- Component relationships
|
||||||
|
- Critical implementation paths
|
||||||
|
|
||||||
|
3. `techContext.md`
|
||||||
|
- Technologies used
|
||||||
|
- Development setup
|
||||||
|
- Technical constraints
|
||||||
|
- Dependencies
|
||||||
|
- Tool usage patterns
|
||||||
|
|
||||||
|
### Additional Context
|
||||||
|
Create additional files/folders within memory-bank/ when they help organize:
|
||||||
|
- Complex feature documentation
|
||||||
|
- Integration specifications
|
||||||
|
- API documentation
|
||||||
|
- Testing strategies
|
||||||
|
- Deployment procedures
|
||||||
|
|
||||||
|
## Core Workflows
|
||||||
|
|
||||||
|
### Plan Mode
|
||||||
|
flowchart TD
|
||||||
|
Start[Start] --> ReadFiles[Read Memory Bank]
|
||||||
|
ReadFiles --> CheckFiles{Files Complete?}
|
||||||
|
|
||||||
|
CheckFiles -->|No| Plan[Create Plan]
|
||||||
|
Plan --> Document[Document in Chat]
|
||||||
|
|
||||||
|
CheckFiles -->|Yes| Verify[Verify Context]
|
||||||
|
Verify --> Strategy[Develop Strategy]
|
||||||
|
Strategy --> Present[Present Approach]
|
||||||
|
|
||||||
|
### Act Mode
|
||||||
|
flowchart TD
|
||||||
|
Start[Start] --> Context[Check Memory Bank]
|
||||||
|
Context --> Update[Update Documentation]
|
||||||
|
Update --> Execute[Execute Task]
|
||||||
|
Execute --> Document[Document Changes]
|
||||||
|
|
||||||
|
## Documentation Updates
|
||||||
|
|
||||||
|
Memory Bank updates occur when:
|
||||||
|
1. Discovering new project patterns
|
||||||
|
2. After implementing significant changes
|
||||||
|
3. When user requests with **update memory bank** (MUST review ALL files)
|
||||||
|
4. When context needs clarification
|
||||||
|
|
||||||
|
flowchart TD
|
||||||
|
Start[Update Process]
|
||||||
|
|
||||||
|
subgraph Process
|
||||||
|
P1[Review ALL Files]
|
||||||
|
P2[Document Current State]
|
||||||
|
P3[Clarify Next Steps]
|
||||||
|
P4[Document Insights & Patterns]
|
||||||
|
|
||||||
|
P1 --> P2 --> P3 --> P4
|
||||||
|
end
|
||||||
|
|
||||||
|
Start --> Process
|
||||||
|
|
||||||
|
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.
|
||||||
|
|
||||||
|
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
|
35
memory-bank/projectbrief.md
Normal file
35
memory-bank/projectbrief.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Project Brief: Merchant Operator Web Application
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
A client-side React application for Merchant owners to manage menus, track orders, and handle operations.
|
||||||
|
|
||||||
|
Merchant Operator App is a browser-only React SPA that lets local businesses create, edit, and audit their own merchant records on Local—a community-commerce network running on the peer-to-peer FogBox stack. Its mission is to decentralize ownership of commercial data and return technological control to merchants and their customers. Data is written locally first, then synchronised through FogBox nodes—no central server—ensuring autonomy and resilience. Open, well-documented APIs let any citizen-developer extend, fork, or integrate the codebase without gatekeepers.
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
### Menu Management
|
||||||
|
- Create and edit menu items (name, price, description, image)
|
||||||
|
- Configure options (size, dough type) and additions/toppings
|
||||||
|
- Set availability times
|
||||||
|
|
||||||
|
### Order Management
|
||||||
|
- View active orders
|
||||||
|
- Track order status
|
||||||
|
- Process fulfillment
|
||||||
|
|
||||||
|
### Merchant Operations
|
||||||
|
- Dashboard with key metrics
|
||||||
|
- Seller management
|
||||||
|
- Security controls
|
||||||
|
|
||||||
|
## UI Design
|
||||||
|
- Dark-themed interface
|
||||||
|
- Sidebar navigation
|
||||||
|
- Responsive layout
|
||||||
|
- Intuitive forms
|
||||||
|
|
||||||
|
## Technical Approach
|
||||||
|
- React with TypeScript
|
||||||
|
- Effector for state management
|
||||||
|
- Tailwind CSS for styling
|
||||||
|
- Component-based architecture
|
54
memory-bank/systemPatterns.md
Normal file
54
memory-bank/systemPatterns.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# System Patterns: Merchant Operator Web Application
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
The application follows a domain-driven, Effector-based Flux architecture.
|
||||||
|
|
||||||
|
### Domain Structure
|
||||||
|
Each feature domain in `src/domains/<feature>/` contains:
|
||||||
|
- `model.ts`: Effector events, stores, and effects (pure, testable)
|
||||||
|
- `view-model.ts`: React hooks (`useStore`/`useUnit`) adapting model logic to components
|
||||||
|
- `ui/`: Dumb presentational React components
|
||||||
|
|
||||||
|
The application entry point (`<App />`) and routing are in `src/app/root.tsx` and `src/app/routes.tsx`.
|
||||||
|
|
||||||
|
## Key Design Patterns
|
||||||
|
|
||||||
|
### Effector-based Flux
|
||||||
|
- **Events**: `createEvent()` represent actions triggered by the user or effects
|
||||||
|
- **Stores**: `createStore()` hold application state and update via `.on(event, ...)`
|
||||||
|
- **Effects**: `createEffect()` handle async side-effects, dispatching success/fail events
|
||||||
|
- **Domains**: Group related events, stores, and effects into isolated modules
|
||||||
|
|
||||||
|
### ViewModel Layer
|
||||||
|
- Encapsulates Effector usage in thin hooks (`view-model.ts`)
|
||||||
|
- Components consume view-model hooks and remain focused on presentation
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
- Design-system atoms in `src/shared/ui/`
|
||||||
|
- Domain-specific components in `src/domains/<feature>/ui/`
|
||||||
|
- Core UI components include: Button, Input, Textarea, Switch, Tabs, ImageUpload, Card, Dialog
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
```
|
||||||
|
event ───┐
|
||||||
|
│ triggers
|
||||||
|
store ───┤ updates ──> view-model hook ──> React component render
|
||||||
|
│
|
||||||
|
effect <─┘ (async; dispatches done/fail events)
|
||||||
|
```
|
||||||
|
User interactions invoke events, stores update state, view-model hooks propagate changes to the UI.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
- OpenID Connect (OIDC) for authentication via dynamic `/oidc.json` configuration
|
||||||
|
- `AuthProvider` initializes auth flow; tokens stored in `localStorage` with auto-refresh
|
||||||
|
- Protected routes using the `ProtectedRoute` component with optional `requiredRoles`
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
- Path-agnostic SPA deployment with Vite and Docker
|
||||||
|
- `BASE_PATH` injected into `public/config.js` at container startup by `docker-entrypoint.sh`
|
||||||
|
- Client-side environment variables (`CLIENT_*`) surfaced on `window.appConfig`
|
||||||
|
- Asset paths managed via `getAssetPath()` in `src/utils/config.ts`
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
- Tailwind CSS configured via `tailwind.config.js`
|
||||||
|
- Utility-first styling with dark theme support and a consistent design system
|
85
memory-bank/techContext.md
Normal file
85
memory-bank/techContext.md
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# Technical Context: Merchant Operator Web Application
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
### Core Technologies
|
||||||
|
- React (Client-side rendering, SPA)
|
||||||
|
- TypeScript (strict mode enabled)
|
||||||
|
- Vite (build tool, dev server)
|
||||||
|
- Tailwind CSS (utility-first styling, dark theme)
|
||||||
|
- Effector (Flux-style state management)
|
||||||
|
|
||||||
|
### Authentication & API
|
||||||
|
- OpenID Connect (OIDC) via dynamic `/oidc.json`
|
||||||
|
- `useApiService` hook for authenticated API calls
|
||||||
|
- Tokens stored in `localStorage`, auto-refresh on expiration
|
||||||
|
|
||||||
|
### Development & Testing Tools
|
||||||
|
- ESLint (linting)
|
||||||
|
- Prettier (formatting)
|
||||||
|
- Husky (Git hooks)
|
||||||
|
- Jest & Testing Library (unit and integration tests)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
app/
|
||||||
|
root.tsx # <App /> entry and routing
|
||||||
|
routes.tsx # Route definitions
|
||||||
|
domains/
|
||||||
|
<feature>/
|
||||||
|
model.ts # Effector events, stores, effects (pure, testable)
|
||||||
|
view-model.ts # Hooks adapting model to React
|
||||||
|
ui/ # Presentational components
|
||||||
|
shared/
|
||||||
|
ui/ # Design-system atoms
|
||||||
|
lib/ # Utilities and helpers
|
||||||
|
services/
|
||||||
|
apiService.ts # Authenticated API wrapper
|
||||||
|
utils/
|
||||||
|
config.ts # getBasePath(), getAssetPath()
|
||||||
|
components/ # Miscellaneous components (if any)
|
||||||
|
public/
|
||||||
|
config.js # Base path placeholder replaced at runtime
|
||||||
|
oidc.json # Dynamic OIDC config
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Organization
|
||||||
|
- **Feature domains**: Self-contained under `src/domains/<feature>`
|
||||||
|
- **ViewModel layer**: Thin hooks (`view-model.ts`) encapsulate Effector logic
|
||||||
|
- **UI components**: Presentational, located in each domain’s `ui/` folder
|
||||||
|
- **Shared UI atoms**: In `src/shared/ui`, for consistent design
|
||||||
|
|
||||||
|
## State Management with Effector
|
||||||
|
- **Events** (`createEvent`): Actions triggered by user or effects
|
||||||
|
- **Stores** (`createStore`): Immutable state containers updated via `.on(...)`
|
||||||
|
- **Effects** (`createEffect`): Async side-effects dispatching done/fail events
|
||||||
|
- **Advanced patterns**: Derived stores, combine, reset events
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
- **OIDC Flow**: `AuthProvider` initializes using `/oidc.json`
|
||||||
|
- **ProtectedRoute**: Secures routes, supports role-based access
|
||||||
|
- **useAuth** & **useApiService** hooks manage login, logout, token handling
|
||||||
|
|
||||||
|
## Deployment & Environment Variables
|
||||||
|
- **Path-agnostic SPA**: `BASE_PATH` injected into `public/config.js` by `docker-entrypoint.sh`
|
||||||
|
- **Client-side vars** (`CLIENT_BASE_PATH`, `CLIENT_API_URL`, `CLIENT_APP_NAME`, `CLIENT_DEBUG`) exposed on `window.appConfig`
|
||||||
|
- **Asset utils**: `getBasePath()` and `getAssetPath()` in `src/utils/config.ts`
|
||||||
|
|
||||||
|
## Coding Guidelines
|
||||||
|
- **TypeScript strict mode**: Ensures type safety
|
||||||
|
- **Pure domain logic**: In `model.ts`, avoid side-effects
|
||||||
|
- **Named exports only**: Prevents default-export rename issues
|
||||||
|
- **Small files, single responsibility**: Easier automated refactoring
|
||||||
|
- **Co-located tests**: Provide regression safety
|
||||||
|
- **Runtime schema checks** (e.g., Zod): Fail fast on invalid data
|
||||||
|
|
||||||
|
## Architecture Rationale
|
||||||
|
Flux with Effector was chosen over MVVM for:
|
||||||
|
- Native React integration and unidirectional data flow
|
||||||
|
- Minimal boilerplate and predictable state management
|
||||||
|
- High testability and TypeScript support
|
||||||
|
- Lower complexity for automated refactors
|
||||||
|
|
||||||
|
Refer to `docs/flux-vs-mvvm.md` for the decision matrix and detailed comparison.
|
27
nginx.conf
Normal file
27
nginx.conf
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
server {
|
||||||
|
listen 369;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
absolute_redirect off;
|
||||||
|
|
||||||
|
# This is needed for path-agnostic SPA deployment
|
||||||
|
# It allows the app to be served from any base path
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle assets with or without base path
|
||||||
|
location ~ ^/assets/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Also serve assets from the base path
|
||||||
|
location ~ ^/[^/]+/assets/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
}
|
7306
package-lock.json
generated
Normal file
7306
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
68
package.json
Normal file
68
package.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "merchant-operator",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@g1/sse-client": "^0.2.0",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.7",
|
||||||
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
|
"@tailwindcss/postcss": "^4.1.4",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"effector": "^23.3.0",
|
||||||
|
"effector-react": "^23.3.0",
|
||||||
|
"lucide-react": "^0.488.0",
|
||||||
|
"oidc-client-ts": "^3.2.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-feather": "^2.0.10",
|
||||||
|
"react-oidc-context": "^3.3.0",
|
||||||
|
"react-router-dom": "^7.5.0",
|
||||||
|
"tailwind-merge": "^3.2.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.26.9",
|
||||||
|
"@babel/plugin-transform-react-jsx": "^7.25.9",
|
||||||
|
"@babel/preset-env": "^7.26.9",
|
||||||
|
"@babel/preset-react": "^7.26.3",
|
||||||
|
"@commitlint/cli": "^19.8.0",
|
||||||
|
"@commitlint/config-conventional": "^19.8.0",
|
||||||
|
"@commitlint/types": "^19.8.0",
|
||||||
|
"@g1/api-generator": "latest",
|
||||||
|
"@types/react": "^18.2.56",
|
||||||
|
"@types/react-dom": "^18.2.19",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^15.5.1",
|
||||||
|
"postcss": "^8.4.33",
|
||||||
|
"postcss-load-config": "^5.0.3",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"vite": "^5.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"dev:path": "CLIENT_BASE_PATH=/Merchant-app vite",
|
||||||
|
"dev:custom": "node vite.server.js",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"build:path": "tsc && CLIENT_BASE_PATH=/Merchant-app vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"preview:path": "CLIENT_BASE_PATH=/Merchant-app vite preview",
|
||||||
|
"prepare": "husky",
|
||||||
|
"api:generate": "g1-api-generator https://localhost:7205/openapi/schema.json src/lib/api/merchant --skip-tls-verify",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"generate-app-config": "node scripts/generate-app-config.js",
|
||||||
|
"generate-app-config:prod": "cross-env NODE_ENV=production node scripts/generate-app-config.js"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,ts,tsx}": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
9
public/app-config.js
Normal file
9
public/app-config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Runtime environment variables - Generated by generate-app-config.js
|
||||||
|
window.appConfig = {
|
||||||
|
apiUrl: "https://localhost:7205",
|
||||||
|
appName: "Merchant Operator App",
|
||||||
|
basePath: "/",
|
||||||
|
debug: true,
|
||||||
|
mapTileApiKey: "pk.19d62daa998822ac22861d96a0424b58",
|
||||||
|
mapTileEndpoint: "https://tiles.locationiq.com/v3/streets/vector.json",
|
||||||
|
};
|
8
react-spa-template.code-workspace
Normal file
8
react-spa-template.code-workspace
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
179
scripts/generate-app-config.js
Normal file
179
scripts/generate-app-config.js
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This script generates a app-config.js file with environment variables
|
||||||
|
* starting with CLIENT_ for both development and production environments.
|
||||||
|
*
|
||||||
|
* It works in both local development and production environments,
|
||||||
|
* eliminating the need for a separate Docker entrypoint script.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
// Get the directory name in ESM
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// Determine if we're in production or development mode
|
||||||
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
|
// Load environment variables from .env files
|
||||||
|
// In production, we prioritize .env.production and .env
|
||||||
|
// In development, we use the standard Vite hierarchy
|
||||||
|
if (isProduction) {
|
||||||
|
dotenv.config({ path: path.resolve(process.cwd(), ".env.production") });
|
||||||
|
dotenv.config({ path: path.resolve(process.cwd(), ".env") });
|
||||||
|
} else {
|
||||||
|
// Development mode - follow Vite's loading order
|
||||||
|
dotenv.config({ path: path.resolve(process.cwd(), ".env") });
|
||||||
|
dotenv.config({ path: path.resolve(process.cwd(), ".env.development") });
|
||||||
|
dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });
|
||||||
|
dotenv.config({
|
||||||
|
path: path.resolve(process.cwd(), ".env.development.local"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the target directory based on environment
|
||||||
|
// In production, we use the dist directory (or a specified output directory)
|
||||||
|
// In development, we use the public directory
|
||||||
|
const targetDir = isProduction
|
||||||
|
? process.env.OUTPUT_DIR || path.resolve(process.cwd(), "dist")
|
||||||
|
: path.resolve(process.cwd(), "public");
|
||||||
|
|
||||||
|
// Ensure the target directory exists
|
||||||
|
if (!fs.existsSync(targetDir)) {
|
||||||
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
|
console.log(`Created directory: ${targetDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to convert SNAKE_CASE to camelCase
|
||||||
|
function toCamelCase(str) {
|
||||||
|
return str
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the app-config.js file
|
||||||
|
const configFilePath = path.join(targetDir, "app-config.js");
|
||||||
|
let configContent =
|
||||||
|
"// Runtime environment variables - Generated by generate-app-config.js\n";
|
||||||
|
configContent += "window.appConfig = {\n";
|
||||||
|
|
||||||
|
// Get all environment variables starting with CLIENT_
|
||||||
|
const clientEnvVars = Object.entries(process.env).filter(([key]) =>
|
||||||
|
key.startsWith("CLIENT_"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add each environment variable to the config
|
||||||
|
clientEnvVars.forEach(([key, value]) => {
|
||||||
|
// Remove CLIENT_ prefix
|
||||||
|
const keyWithoutPrefix = key.replace(/^CLIENT_/, "");
|
||||||
|
|
||||||
|
// Convert to camelCase
|
||||||
|
const camelKey = toCamelCase(keyWithoutPrefix);
|
||||||
|
|
||||||
|
// Add the key-value pair to the config
|
||||||
|
// If the value is 'true', 'false', 'null', or a number, don't add quotes
|
||||||
|
if (
|
||||||
|
value === "true" ||
|
||||||
|
value === "false" ||
|
||||||
|
value === "null" ||
|
||||||
|
/^[0-9]+$/.test(value)
|
||||||
|
) {
|
||||||
|
configContent += ` ${camelKey}: ${value},\n`;
|
||||||
|
} else {
|
||||||
|
configContent += ` ${camelKey}: "${value}",\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add default values for essential variables if they don't exist
|
||||||
|
if (!process.env.CLIENT_BASE_PATH) {
|
||||||
|
configContent += ' basePath: "/",\n';
|
||||||
|
}
|
||||||
|
if (!process.env.CLIENT_APP_NAME) {
|
||||||
|
configContent += ' appName: "Merchant Operator App",\n';
|
||||||
|
}
|
||||||
|
if (!process.env.CLIENT_DEBUG) {
|
||||||
|
configContent += " debug: " + (isProduction ? "false" : "true") + ",\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the object
|
||||||
|
configContent += "};";
|
||||||
|
|
||||||
|
// Helper function for colorized logging like Vite
|
||||||
|
function formatLog(label, message, color = "\x1b[36m ") {
|
||||||
|
// Default to cyan color
|
||||||
|
const reset = "\x1b[0m";
|
||||||
|
const dim = "\x1b[2m";
|
||||||
|
const arrow = `${dim}${color}➜${reset}`;
|
||||||
|
const formattedLabel = `${color}${label}:${reset}`;
|
||||||
|
return ` ${arrow} ${formattedLabel} ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the config file
|
||||||
|
fs.writeFileSync(configFilePath, configContent);
|
||||||
|
console.log(formatLog("Generated", `app-config.js at ${configFilePath}`));
|
||||||
|
|
||||||
|
// Check if index.html exists in the target directory
|
||||||
|
const indexHtmlPath = path.join(targetDir, "index.html");
|
||||||
|
if (fs.existsSync(indexHtmlPath)) {
|
||||||
|
let indexHtmlContent = fs.readFileSync(indexHtmlPath, "utf8");
|
||||||
|
|
||||||
|
// Check if the script tag already exists
|
||||||
|
if (!indexHtmlContent.includes("app-config.js")) {
|
||||||
|
// Insert the script tag after the opening head tag
|
||||||
|
indexHtmlContent = indexHtmlContent.replace(
|
||||||
|
"<head>",
|
||||||
|
'<head>\n <script src="./app-config.js"></script>',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the updated index.html
|
||||||
|
fs.writeFileSync(indexHtmlPath, indexHtmlContent);
|
||||||
|
console.log(
|
||||||
|
formatLog(
|
||||||
|
"Updated",
|
||||||
|
`injected script tag into ${path.basename(indexHtmlPath)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
formatLog(
|
||||||
|
"Note",
|
||||||
|
`app-config.js script already exists in ${path.basename(indexHtmlPath)}`,
|
||||||
|
"\x1b[33m ",
|
||||||
|
),
|
||||||
|
); // Yellow
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
formatLog("Note", `index.html not found in ${targetDir}`, "\x1b[34m "),
|
||||||
|
); // Blue
|
||||||
|
if (!isProduction) {
|
||||||
|
console.log(
|
||||||
|
formatLog(
|
||||||
|
"Vite",
|
||||||
|
"script will be injected during development",
|
||||||
|
"\x1b[34m ",
|
||||||
|
),
|
||||||
|
); // Blue
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
formatLog(
|
||||||
|
"Warning",
|
||||||
|
"index.html not found in production build directory!",
|
||||||
|
"\x1b[33m ",
|
||||||
|
),
|
||||||
|
); // Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
formatLog(
|
||||||
|
"Ready",
|
||||||
|
`app-config generated for ${isProduction ? "production" : "development"} environment`,
|
||||||
|
"\x1b[32m ",
|
||||||
|
),
|
||||||
|
); // Green
|
98
scripts/vite-plugin-app-config.js
Normal file
98
scripts/vite-plugin-app-config.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Vite plugin to inject app-config.js into the HTML head during development
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
|
||||||
|
// Get the directory name in ESM
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// Helper function for colorized logging like Vite
|
||||||
|
function formatLog(label, message, color = "\x1b[36m ") {
|
||||||
|
// Default to cyan color
|
||||||
|
const reset = "\x1b[0m";
|
||||||
|
const dim = "\x1b[2m";
|
||||||
|
const arrow = `${dim}${color}➜${reset}`;
|
||||||
|
const formattedLabel = `${color}${label}:${reset}`;
|
||||||
|
return ` ${arrow} ${formattedLabel} ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Vite plugin that injects the app-config.js script into the HTML head
|
||||||
|
* @returns {import('vite').Plugin}
|
||||||
|
*/
|
||||||
|
function appConfigPlugin() {
|
||||||
|
return {
|
||||||
|
name: "vite-plugin-app-config",
|
||||||
|
|
||||||
|
configureServer(server) {
|
||||||
|
// Run the generate-app-config.js script when the dev server starts
|
||||||
|
const scriptPath = path.resolve(__dirname, "generate-app-config.js");
|
||||||
|
const generateConfig = () => {
|
||||||
|
const process = spawn("node", [scriptPath], {
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("error", (err) => {
|
||||||
|
console.error(
|
||||||
|
formatLog(
|
||||||
|
"Error",
|
||||||
|
`Failed to run generate-app-config.js: ${err}`,
|
||||||
|
"\x1b[31m ",
|
||||||
|
),
|
||||||
|
); // Red
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate config on server start
|
||||||
|
generateConfig();
|
||||||
|
|
||||||
|
// Watch for changes in .env files and regenerate app-config.js
|
||||||
|
const envFiles = [
|
||||||
|
".env",
|
||||||
|
".env.development",
|
||||||
|
".env.local",
|
||||||
|
".env.development.local",
|
||||||
|
];
|
||||||
|
|
||||||
|
envFiles.forEach((file) => {
|
||||||
|
const filePath = path.resolve(process.cwd(), file);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
server.watcher.add(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.watcher.on("change", (file) => {
|
||||||
|
if (envFiles.some((envFile) => file.endsWith(envFile))) {
|
||||||
|
console.log(
|
||||||
|
formatLog(
|
||||||
|
"Changed",
|
||||||
|
`ENV file: ${path.basename(file)}`,
|
||||||
|
"\x1b[34m ",
|
||||||
|
),
|
||||||
|
); // Blue
|
||||||
|
generateConfig();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
transformIndexHtml(html) {
|
||||||
|
// Check if the app-config.js script is already in the HTML
|
||||||
|
if (!html.includes("app-config.js")) {
|
||||||
|
// Insert the script tag after the opening head tag
|
||||||
|
return html.replace(
|
||||||
|
"<head>",
|
||||||
|
'<head>\n <script src="./app-config.js"></script>',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default appConfigPlugin;
|
13
src/app/root.tsx
Normal file
13
src/app/root.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { AuthProvider } from "@domains/auth";
|
||||||
|
import { AppRouter } from "./routes";
|
||||||
|
|
||||||
|
export const Root: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<React.StrictMode>
|
||||||
|
<AuthProvider>
|
||||||
|
<AppRouter />
|
||||||
|
</AuthProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
};
|
38
src/app/routes.tsx
Normal file
38
src/app/routes.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { createHashRouter, RouterProvider, Outlet } from "react-router-dom";
|
||||||
|
import { ProtectedRoute, LoginPage, AuthCallback } from "@domains/auth";
|
||||||
|
import { Dashboard } from "@domains/dashboard";
|
||||||
|
import { MainLayout } from "@shared/ui";
|
||||||
|
|
||||||
|
// Define routes
|
||||||
|
const router = createHashRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: (
|
||||||
|
<ProtectedRoute>
|
||||||
|
<MainLayout>
|
||||||
|
<Outlet />
|
||||||
|
</MainLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <Dashboard />,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
element: <LoginPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/auth/callback",
|
||||||
|
element: <AuthCallback />,
|
||||||
|
},
|
||||||
|
// Add other routes as needed
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const AppRouter: React.FC = () => {
|
||||||
|
return <RouterProvider router={router} />;
|
||||||
|
};
|
12
src/domains/auth/index.ts
Normal file
12
src/domains/auth/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Export model
|
||||||
|
export * from "./model";
|
||||||
|
|
||||||
|
// Export view-model
|
||||||
|
export * from "./view-model";
|
||||||
|
|
||||||
|
// Export UI components
|
||||||
|
export * from "./ui";
|
||||||
|
export { AuthCallback } from "./ui/AuthCallback"; // Add export for AuthCallback
|
||||||
|
|
||||||
|
// Re-export AuthProvider from UI
|
||||||
|
export { AuthProvider } from "./ui";
|
341
src/domains/auth/model.ts
Normal file
341
src/domains/auth/model.ts
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
import { createStore, createEvent, createEffect } from "effector";
|
||||||
|
import { User, IdTokenClaims } from "oidc-client-ts"; // UserManager removed
|
||||||
|
import { getUserManager } from "./oidc-config"; // Import getUserManager
|
||||||
|
|
||||||
|
// Define types
|
||||||
|
export interface AppUser {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
roles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OidcUserProfile {
|
||||||
|
sub: string;
|
||||||
|
name?: string;
|
||||||
|
preferred_username?: string;
|
||||||
|
email?: string;
|
||||||
|
roles?: string[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OidcUser extends Omit<User, "profile"> {
|
||||||
|
profile: OidcUserProfile & Partial<IdTokenClaims>;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthState {
|
||||||
|
user: AppUser | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
manuallyLoggedOut: boolean; // Track if user manually logged out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define events
|
||||||
|
export const setUser = createEvent<AppUser | null>();
|
||||||
|
export const setIsLoading = createEvent<boolean>();
|
||||||
|
export const setError = createEvent<string | null>();
|
||||||
|
export const logout = createEvent();
|
||||||
|
|
||||||
|
// Define effects
|
||||||
|
export const loginFx = createEffect(async () => {
|
||||||
|
const userManager = getUserManager(); // Get instance
|
||||||
|
|
||||||
|
// First try to get the current user
|
||||||
|
const currentUser = await userManager.getUser();
|
||||||
|
|
||||||
|
// If we have a valid user that's not expired, we're already logged in
|
||||||
|
if (currentUser && !currentUser.expired) {
|
||||||
|
console.log("loginFx: User is already logged in and token is valid");
|
||||||
|
return currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try silent login first
|
||||||
|
try {
|
||||||
|
console.log("loginFx: Attempting silent login");
|
||||||
|
const user = await userManager.signinSilent();
|
||||||
|
console.log("loginFx: Silent login successful");
|
||||||
|
return user;
|
||||||
|
} catch (silentError) {
|
||||||
|
console.log(
|
||||||
|
"loginFx: Silent login failed, falling back to redirect:",
|
||||||
|
silentError,
|
||||||
|
);
|
||||||
|
// Fall back to redirect login
|
||||||
|
await userManager.signinRedirect();
|
||||||
|
return null; // This line won't be reached as signinRedirect redirects the page
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const logoutFx = createEffect(async () => {
|
||||||
|
// Set the manuallyLoggedOut flag in localStorage first
|
||||||
|
setManuallyLoggedOutInStorage(true);
|
||||||
|
console.log("logoutFx: Set manuallyLoggedOut to true in localStorage");
|
||||||
|
|
||||||
|
// Clear the user from the store before redirecting
|
||||||
|
// This will prevent auto-login from triggering
|
||||||
|
logout();
|
||||||
|
|
||||||
|
// Get the UserManager instance
|
||||||
|
const userManager = getUserManager();
|
||||||
|
|
||||||
|
// Use a small delay to ensure the flag is set before redirecting
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// Now redirect to the identity provider for logout
|
||||||
|
await userManager.signoutRedirect();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const checkAuthFx = createEffect<void, OidcUser | null, Error>(
|
||||||
|
async () => {
|
||||||
|
console.log("checkAuthFx: Starting to check authentication status");
|
||||||
|
try {
|
||||||
|
const userManager = getUserManager(); // Get instance
|
||||||
|
console.log("checkAuthFx: Got UserManager instance");
|
||||||
|
|
||||||
|
// First try to get the user from storage
|
||||||
|
let user = await userManager.getUser();
|
||||||
|
console.log("checkAuthFx: User from storage:", user);
|
||||||
|
|
||||||
|
// If we have a user but the token is expired or about to expire, try silent renew
|
||||||
|
if (
|
||||||
|
user &&
|
||||||
|
(user.expired || (user.expires_in && user.expires_in < 300))
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"checkAuthFx: Token is expired or about to expire, attempting silent renew",
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
user = await userManager.signinSilent();
|
||||||
|
console.log("checkAuthFx: Silent renew successful, new user:", user);
|
||||||
|
} catch (renewError) {
|
||||||
|
console.warn("checkAuthFx: Silent renew failed:", renewError);
|
||||||
|
// Continue with the expired user, the app will handle redirect if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.log("checkAuthFx: No user found, user is not authenticated");
|
||||||
|
} else {
|
||||||
|
console.log("checkAuthFx: User found, user is authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
return user as OidcUser | null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("checkAuthFx: Error checking authentication:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const handleCallbackFx = createEffect<void, OidcUser, Error>(
|
||||||
|
async () => {
|
||||||
|
console.log("handleCallbackFx: Starting to process callback");
|
||||||
|
try {
|
||||||
|
const userManager = getUserManager(); // Get instance
|
||||||
|
console.log("handleCallbackFx: Got UserManager instance");
|
||||||
|
|
||||||
|
const user = await userManager.signinRedirectCallback();
|
||||||
|
console.log(
|
||||||
|
"handleCallbackFx: Successfully processed callback, user:",
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
|
||||||
|
return user as OidcUser;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("handleCallbackFx: Error processing callback:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Helper functions for localStorage
|
||||||
|
const getManuallyLoggedOutFromStorage = (): boolean => {
|
||||||
|
try {
|
||||||
|
const value = localStorage.getItem("auth_manually_logged_out");
|
||||||
|
return value === "true";
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error reading from localStorage:", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setManuallyLoggedOutInStorage = (value: boolean): void => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem("auth_manually_logged_out", value.toString());
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error writing to localStorage:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define initial state
|
||||||
|
const initialState: AuthState = {
|
||||||
|
user: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
manuallyLoggedOut: getManuallyLoggedOutFromStorage(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create store
|
||||||
|
export const $auth = createStore<AuthState>(initialState)
|
||||||
|
.on(checkAuthFx.pending, (state) => {
|
||||||
|
console.log("checkAuthFx.pending handler called", { prevState: state });
|
||||||
|
return {
|
||||||
|
// Add pending handler
|
||||||
|
...state,
|
||||||
|
isLoading: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.on(checkAuthFx.doneData, (state, result) => {
|
||||||
|
console.log("checkAuthFx.doneData handler called", {
|
||||||
|
result,
|
||||||
|
prevState: state,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If a user is found, reset the manuallyLoggedOut flag in localStorage
|
||||||
|
if (result) {
|
||||||
|
setManuallyLoggedOutInStorage(false);
|
||||||
|
console.log(
|
||||||
|
"checkAuthFx.doneData: User found, reset manuallyLoggedOut to false in localStorage",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
"checkAuthFx.doneData: No user found, keeping manuallyLoggedOut as",
|
||||||
|
state.manuallyLoggedOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: false, // Set loading false on done
|
||||||
|
user: result
|
||||||
|
? {
|
||||||
|
id: result.profile.sub,
|
||||||
|
name:
|
||||||
|
result.profile.name || result.profile.preferred_username || "",
|
||||||
|
email: result.profile.email || "",
|
||||||
|
roles: result.profile.roles || [],
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
isAuthenticated: !!result,
|
||||||
|
// If a user is found, reset the manuallyLoggedOut flag
|
||||||
|
manuallyLoggedOut: result ? false : state.manuallyLoggedOut,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.on(checkAuthFx.fail, (state, { error }) => {
|
||||||
|
console.log("checkAuthFx.fail handler called", { error, prevState: state });
|
||||||
|
return {
|
||||||
|
// Add fail handler
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.on(handleCallbackFx.pending, (state) => {
|
||||||
|
console.log("handleCallbackFx.pending handler called", {
|
||||||
|
prevState: state,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
// Add pending handler for callback
|
||||||
|
...state,
|
||||||
|
isLoading: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.on(handleCallbackFx.doneData, (state, result) => {
|
||||||
|
console.log("handleCallbackFx.doneData handler called", {
|
||||||
|
result,
|
||||||
|
prevState: state,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset flag in localStorage when user successfully logs in
|
||||||
|
setManuallyLoggedOutInStorage(false);
|
||||||
|
console.log(
|
||||||
|
"handleCallbackFx.doneData: Reset manuallyLoggedOut to false in localStorage",
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: false, // Set loading false on done
|
||||||
|
user: {
|
||||||
|
id: result.profile.sub,
|
||||||
|
name: result.profile.name || result.profile.preferred_username || "",
|
||||||
|
email: result.profile.email || "",
|
||||||
|
roles: result.profile.roles || [],
|
||||||
|
},
|
||||||
|
isAuthenticated: true,
|
||||||
|
manuallyLoggedOut: false, // Reset the flag when user successfully logs in
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.on(handleCallbackFx.fail, (state, { error }) => {
|
||||||
|
console.log("handleCallbackFx.fail handler called", {
|
||||||
|
error,
|
||||||
|
prevState: state,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
// Add fail handler for callback
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.on(setUser, (state, user) => ({
|
||||||
|
...state,
|
||||||
|
user,
|
||||||
|
isAuthenticated: !!user,
|
||||||
|
}))
|
||||||
|
.on(setIsLoading, (state, isLoading) => ({
|
||||||
|
...state,
|
||||||
|
isLoading,
|
||||||
|
}))
|
||||||
|
.on(setError, (state, error) => ({
|
||||||
|
...state,
|
||||||
|
error,
|
||||||
|
}))
|
||||||
|
.on(loginFx.pending, (state) => ({
|
||||||
|
...state,
|
||||||
|
isLoading: true,
|
||||||
|
error: null,
|
||||||
|
}))
|
||||||
|
.on(loginFx.done, (state) => ({
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
}))
|
||||||
|
.on(loginFx.fail, (state, { error }) => ({
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
error: error.message,
|
||||||
|
}))
|
||||||
|
.on(logoutFx.pending, (state) => ({
|
||||||
|
...state,
|
||||||
|
isLoading: true,
|
||||||
|
}))
|
||||||
|
.on(logoutFx.done, () => {
|
||||||
|
// Set flag in localStorage
|
||||||
|
setManuallyLoggedOutInStorage(true);
|
||||||
|
console.log("logoutFx.done: Set manuallyLoggedOut to true in localStorage");
|
||||||
|
|
||||||
|
return {
|
||||||
|
...initialState,
|
||||||
|
manuallyLoggedOut: true, // Set flag when user manually logs out
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.on(logoutFx.fail, (state, { error }) => ({
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
error: error.message,
|
||||||
|
}))
|
||||||
|
.on(logout, () => {
|
||||||
|
// Set flag in localStorage
|
||||||
|
setManuallyLoggedOutInStorage(true);
|
||||||
|
console.log("logout event: Set manuallyLoggedOut to true in localStorage");
|
||||||
|
|
||||||
|
return {
|
||||||
|
...initialState,
|
||||||
|
manuallyLoggedOut: true, // Set flag when user manually logs out
|
||||||
|
};
|
||||||
|
});
|
122
src/domains/auth/oidc-config.ts
Normal file
122
src/domains/auth/oidc-config.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import {
|
||||||
|
UserManager,
|
||||||
|
WebStorageStateStore,
|
||||||
|
UserManagerSettings,
|
||||||
|
} from "oidc-client-ts";
|
||||||
|
import { OidcService } from "@merchant-api/services/OidcService";
|
||||||
|
import { oidcConnectResponseDto } from "@merchant-api/models/oidcConnectResponseDto";
|
||||||
|
import { OpenAPI } from "@merchant-api/core/OpenAPI";
|
||||||
|
import { createHashUrl, normalizeUrl } from "@shared/lib/url-utils";
|
||||||
|
import { getApiUrlSingleton } from "@shared/lib/api-url";
|
||||||
|
|
||||||
|
let userManagerInstance: UserManager | null = null;
|
||||||
|
|
||||||
|
export const getOidcConfig = async (): Promise<UserManagerSettings> => {
|
||||||
|
try {
|
||||||
|
// Get the API URL using our utility function
|
||||||
|
try {
|
||||||
|
// This will handle auto-detection if needed
|
||||||
|
const apiUrl = getApiUrlSingleton();
|
||||||
|
|
||||||
|
console.log(`Using API URL for OIDC config: ${apiUrl}`);
|
||||||
|
// Ensure OpenAPI.BASE is set before making any API calls
|
||||||
|
// This is a safeguard in case the initialize.ts module hasn't run yet
|
||||||
|
OpenAPI.BASE = apiUrl;
|
||||||
|
} catch (apiUrlError) {
|
||||||
|
console.error("FATAL ERROR: Failed to get API URL:", apiUrlError);
|
||||||
|
throw apiUrlError;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Fetching OIDC config using OidcService");
|
||||||
|
|
||||||
|
// Use the OidcService to fetch the OIDC configuration
|
||||||
|
console.log("About to call OidcService.getOidcJson()");
|
||||||
|
let config: oidcConnectResponseDto;
|
||||||
|
try {
|
||||||
|
config = await OidcService.getOidcJson();
|
||||||
|
console.log("OIDC config received:", config);
|
||||||
|
} catch (fetchError) {
|
||||||
|
console.error("Error from OidcService.getOidcJson():", fetchError);
|
||||||
|
console.error(
|
||||||
|
"This may be due to CLIENT_API_URL being incorrect or the API server being unavailable",
|
||||||
|
);
|
||||||
|
console.error(`Current OpenAPI.BASE: ${OpenAPI.BASE}`);
|
||||||
|
throw fetchError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config || !config.authorityUrl || !config.clientId) {
|
||||||
|
console.error(
|
||||||
|
"FATAL ERROR: Invalid OIDC config received from API - missing authorityUrl or clientId",
|
||||||
|
);
|
||||||
|
throw new Error("Invalid OIDC config received from API");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the authorityUrl that will be used for OIDC
|
||||||
|
console.log(`Using OIDC authorityUrl: ${config.authorityUrl}`);
|
||||||
|
|
||||||
|
// Important: We use the authorityUrl directly from the OIDC config
|
||||||
|
// This URL will be used for all identity/.well-known endpoint checks
|
||||||
|
|
||||||
|
// Properly construct the redirect URI using our utility functions
|
||||||
|
const redirectUri = createHashUrl(
|
||||||
|
window.location.origin,
|
||||||
|
window.appConfig?.basePath,
|
||||||
|
"auth/callback",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the post-logout redirect URI
|
||||||
|
const logoutRedirectUri = createHashUrl(
|
||||||
|
window.location.origin,
|
||||||
|
window.appConfig?.basePath,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Normalize the authority URL to ensure it has a trailing slash
|
||||||
|
var normalizedAuthorityUrl = normalizeUrl(config.authorityUrl);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Using normalized OIDC authorityUrl: ${normalizedAuthorityUrl}`,
|
||||||
|
);
|
||||||
|
console.log(`Constructed redirect URI: ${redirectUri}`);
|
||||||
|
console.log(`Constructed logout redirect URI: ${logoutRedirectUri}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
authority: normalizedAuthorityUrl, // Using normalized authorityUrl
|
||||||
|
client_id: config.clientId,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
response_type: "code",
|
||||||
|
scope: "openid profile email offline_access", // Added offline_access for refresh tokens
|
||||||
|
post_logout_redirect_uri: logoutRedirectUri,
|
||||||
|
userStore: new WebStorageStateStore({ store: window.localStorage }),
|
||||||
|
loadUserInfo: true,
|
||||||
|
automaticSilentRenew: true,
|
||||||
|
revokeTokensOnSignout: true,
|
||||||
|
monitorSession: true,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"FATAL ERROR: Failed to initialize OIDC configuration:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw error; // Re-throw to propagate the error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initializeUserManager = async (): Promise<UserManager> => {
|
||||||
|
if (userManagerInstance) {
|
||||||
|
return userManagerInstance;
|
||||||
|
}
|
||||||
|
const settings = await getOidcConfig();
|
||||||
|
userManagerInstance = new UserManager(settings);
|
||||||
|
return userManagerInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to get the initialized instance, throws error if not initialized
|
||||||
|
export const getUserManager = (): UserManager => {
|
||||||
|
if (!userManagerInstance) {
|
||||||
|
throw new Error(
|
||||||
|
"UserManager not initialized. Call initializeUserManager first.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return userManagerInstance;
|
||||||
|
};
|
45
src/domains/auth/types.ts
Normal file
45
src/domains/auth/types.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { User, UserManager } from "oidc-client-ts";
|
||||||
|
|
||||||
|
// Auth User Type
|
||||||
|
export interface AuthUser extends Omit<User, "access_token"> {
|
||||||
|
roles?: string[];
|
||||||
|
resource_access?: {
|
||||||
|
roles?: string[];
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
expires_at?: number;
|
||||||
|
access_token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth Context Type
|
||||||
|
export interface AuthContext {
|
||||||
|
isLoading: boolean;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
user: AuthUser | null;
|
||||||
|
error: Error | null;
|
||||||
|
signinRedirect: () => Promise<void>;
|
||||||
|
signinSilent: () => Promise<void>;
|
||||||
|
removeUser: () => Promise<void>;
|
||||||
|
userManager: UserManager;
|
||||||
|
settings: any;
|
||||||
|
events: any;
|
||||||
|
isSignedIn: boolean;
|
||||||
|
signinPopup: () => Promise<void>;
|
||||||
|
signinSilentCallback: () => Promise<void>;
|
||||||
|
signinPopupCallback: () => Promise<void>;
|
||||||
|
signinRedirectCallback: () => Promise<void>;
|
||||||
|
signoutRedirect: (args?: any) => Promise<void>;
|
||||||
|
signoutRedirectCallback: () => Promise<void>;
|
||||||
|
signoutPopup: (args?: any) => Promise<void>;
|
||||||
|
signoutPopupCallback: () => Promise<void>;
|
||||||
|
signoutSilent: (args?: any) => Promise<void>;
|
||||||
|
signoutSilentCallback: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extended Auth Context Type
|
||||||
|
export interface ExtendedAuthContext extends AuthContext {
|
||||||
|
hasRole: (role: string) => boolean;
|
||||||
|
getAccessToken: () => string | undefined;
|
||||||
|
isTokenExpiringSoon: () => boolean;
|
||||||
|
refreshToken: () => Promise<boolean>;
|
||||||
|
}
|
143
src/domains/auth/ui/AuthCallback.tsx
Normal file
143
src/domains/auth/ui/AuthCallback.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { handleCallbackFx, setIsLoading } from "../model"; // Import the effect and events
|
||||||
|
import { useAuth } from "../view-model";
|
||||||
|
import { cleanupUrl } from "@shared/lib/url-utils";
|
||||||
|
|
||||||
|
export const AuthCallback: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
|
const [status, setStatus] = useState("processing");
|
||||||
|
|
||||||
|
// We're using the imported cleanupUrl function from url-utils
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const processCallback = async () => {
|
||||||
|
// If user is already authenticated, just redirect to home
|
||||||
|
if (isAuthenticated) {
|
||||||
|
console.log(
|
||||||
|
"AuthCallback: User is already authenticated, redirecting to home",
|
||||||
|
);
|
||||||
|
setStatus("success");
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// Redirect to home page
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate("/", { replace: true });
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("AuthCallback: Starting to process callback");
|
||||||
|
// Use the effect to handle the callback and update the store
|
||||||
|
await handleCallbackFx();
|
||||||
|
console.log("AuthCallback: Callback processed successfully");
|
||||||
|
|
||||||
|
// Explicitly set loading to false to prevent loading screen
|
||||||
|
setIsLoading(false);
|
||||||
|
setStatus("success");
|
||||||
|
|
||||||
|
// Redirect to the intended page after successful login
|
||||||
|
// Retrieve the stored return URL or default to '/'
|
||||||
|
const returnUrl = sessionStorage.getItem("oidc_return_url") || "/";
|
||||||
|
sessionStorage.removeItem("oidc_return_url"); // Clean up
|
||||||
|
|
||||||
|
console.log("AuthCallback: Redirecting to", returnUrl);
|
||||||
|
|
||||||
|
// Clean up URL
|
||||||
|
cleanupUrl();
|
||||||
|
|
||||||
|
// Add a small delay before redirecting to ensure state is updated
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate(returnUrl, { replace: true });
|
||||||
|
}, 500);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error handling OIDC callback:", err);
|
||||||
|
|
||||||
|
// If the error is about missing state but user is authenticated,
|
||||||
|
// we can just redirect to home
|
||||||
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
if (
|
||||||
|
errorMessage.includes("No matching state found") &&
|
||||||
|
isAuthenticated
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"AuthCallback: State error but user is authenticated, redirecting to home",
|
||||||
|
);
|
||||||
|
setStatus("success");
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// Clean up URL
|
||||||
|
cleanupUrl();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate("/", { replace: true });
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus("error");
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// Clean up URL
|
||||||
|
cleanupUrl();
|
||||||
|
|
||||||
|
// Redirect to login page on error after a small delay
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate("/login", { replace: true });
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processCallback();
|
||||||
|
}, [navigate, isAuthenticated]);
|
||||||
|
|
||||||
|
// Display a loading indicator while processing the callback using the same design as login page
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen bg-[#131215]">
|
||||||
|
<div className="w-full max-w-md p-8 space-y-8 bg-[#26242a] rounded-lg shadow-lg">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-2xl font-bold text-white">Merchant Operator</h1>
|
||||||
|
|
||||||
|
{status === "processing" && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#653cee] mb-4"></div>
|
||||||
|
<p className="text-white text-lg">Completing login...</p>
|
||||||
|
<p className="text-gray-400 text-sm mt-2">
|
||||||
|
Please wait while we verify your credentials
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === "success" && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<div className="text-green-500 text-5xl mb-4">✓</div>
|
||||||
|
<p className="text-white text-lg">Login successful!</p>
|
||||||
|
<p className="text-gray-400 text-sm mt-2">
|
||||||
|
Redirecting to your dashboard...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === "error" && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<div className="text-red-500 text-5xl mb-4">✗</div>
|
||||||
|
<p className="text-white text-lg">Login failed</p>
|
||||||
|
<p className="text-gray-400 text-sm mt-2">
|
||||||
|
Redirecting to login page...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
145
src/domains/auth/ui/AuthProvider.tsx
Normal file
145
src/domains/auth/ui/AuthProvider.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import React, { ReactNode, useEffect, useState } from "react";
|
||||||
|
import { useAuth } from "../view-model";
|
||||||
|
import { checkAuthFx, setError, setIsLoading } from "../model"; // Import effect and events
|
||||||
|
import { initializeUserManager } from "../oidc-config"; // Import initializer
|
||||||
|
import { checkApiUrlConfig } from "@shared/lib/diagnostics"; // Import diagnostics utility
|
||||||
|
|
||||||
|
interface AuthProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||||
|
const { isLoading, error } = useAuth();
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
|
||||||
|
// Initialize UserManager and check authentication status on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true; // Flag to prevent state updates after unmount
|
||||||
|
|
||||||
|
const initAuth = async () => {
|
||||||
|
try {
|
||||||
|
console.log("AuthProvider: Starting initialization");
|
||||||
|
|
||||||
|
// Run diagnostics to check API URL configuration
|
||||||
|
console.log("AuthProvider: Running API URL diagnostics");
|
||||||
|
const apiUrl = checkApiUrlConfig();
|
||||||
|
if (!apiUrl) {
|
||||||
|
console.error(
|
||||||
|
"AuthProvider: window.appConfig.apiUrl is not defined, authentication will likely fail",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await initializeUserManager(); // Initialize first
|
||||||
|
console.log("AuthProvider: UserManager initialized");
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
const result = await checkAuthFx(); // Then check auth status
|
||||||
|
console.log(
|
||||||
|
"AuthProvider: Authentication check completed",
|
||||||
|
result ? "User authenticated" : "No user",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
setIsInitialized(true);
|
||||||
|
setIsLoading(false); // Explicitly set loading to false
|
||||||
|
console.log(
|
||||||
|
"AuthProvider: Initialization complete, isInitialized set to true, isLoading set to false",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (initError) {
|
||||||
|
console.error(
|
||||||
|
"FATAL ERROR: Failed to initialize OIDC or check auth:",
|
||||||
|
initError,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
setError(
|
||||||
|
initError instanceof Error
|
||||||
|
? initError.message
|
||||||
|
: "Authentication initialization failed",
|
||||||
|
);
|
||||||
|
setIsInitialized(true); // Mark as initialized even on error to stop loading screen
|
||||||
|
setIsLoading(false); // Explicitly set loading to false
|
||||||
|
console.log(
|
||||||
|
"AuthProvider: Initialization failed, but isInitialized set to true, isLoading set to false",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initAuth();
|
||||||
|
|
||||||
|
// Cleanup function to prevent state updates after unmount
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// If not initialized yet, render nothing (or a minimal loader)
|
||||||
|
if (!isInitialized) {
|
||||||
|
console.log("AuthProvider render: Not initialized yet", {
|
||||||
|
isLoading,
|
||||||
|
isInitialized,
|
||||||
|
});
|
||||||
|
return null; // Return nothing, the login page will handle the loading state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error state
|
||||||
|
if (error) {
|
||||||
|
console.log("AuthProvider render: Showing error state", { error });
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen bg-[#131215]">
|
||||||
|
<div className="w-full max-w-md p-8 space-y-8 bg-[#26242a] rounded-lg shadow-lg">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-2xl font-bold text-white">Merchant Operator</h1>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<div className="text-red-500 text-5xl mb-4">✗</div>
|
||||||
|
<p className="text-white text-lg">Authentication Error</p>
|
||||||
|
<p className="text-red-400 text-sm mt-2 mb-4">{error}</p>
|
||||||
|
|
||||||
|
<div className="text-white bg-[#1a191c] p-4 rounded text-left mb-6 overflow-auto max-h-60 w-full">
|
||||||
|
<p className="mb-2 font-semibold">Possible solutions:</p>
|
||||||
|
<ol className="list-decimal pl-5 space-y-2 text-sm text-gray-300">
|
||||||
|
<li>
|
||||||
|
Check that the API server is running at the URL specified
|
||||||
|
in window.appConfig.apiUrl
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Verify that the /oidc.json endpoint is accessible from the
|
||||||
|
API
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Ensure the OIDC configuration has valid authorityUrl and
|
||||||
|
clientId values
|
||||||
|
</li>
|
||||||
|
<li>Check browser console for detailed error messages</li>
|
||||||
|
<li>
|
||||||
|
If window.appConfig.apiUrl is not defined, check your
|
||||||
|
environment variables and restart the development server
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="w-full px-4 py-2 text-white bg-[#653cee] rounded-md hover:bg-[#4e2eb8] focus:outline-none focus:ring-2 focus:ring-[#653cee] focus:ring-offset-2 focus:ring-offset-[#26242a] transition-colors"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render children when authentication is ready
|
||||||
|
console.log(
|
||||||
|
"AuthProvider render: Rendering children, authentication is ready",
|
||||||
|
);
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
111
src/domains/auth/ui/LoginPage.tsx
Normal file
111
src/domains/auth/ui/LoginPage.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useAuth } from "../view-model";
|
||||||
|
|
||||||
|
export const LoginPage: React.FC = () => {
|
||||||
|
const { login, isLoading, manuallyLoggedOut, isAuthenticated, user } =
|
||||||
|
useAuth();
|
||||||
|
|
||||||
|
// Auto-login when component mounts if not manually logged out and not already authenticated
|
||||||
|
useEffect(() => {
|
||||||
|
// If already authenticated, no need to do anything
|
||||||
|
if (isAuthenticated || user) {
|
||||||
|
console.log(
|
||||||
|
"LoginPage: User is already authenticated, no need to auto-login",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check localStorage directly for the manuallyLoggedOut flag
|
||||||
|
const isManuallyLoggedOut =
|
||||||
|
localStorage.getItem("auth_manually_logged_out") === "true";
|
||||||
|
console.log(
|
||||||
|
"LoginPage: localStorage auth_manually_logged_out =",
|
||||||
|
isManuallyLoggedOut,
|
||||||
|
);
|
||||||
|
console.log("LoginPage: manuallyLoggedOut from state =", manuallyLoggedOut);
|
||||||
|
|
||||||
|
// Only auto-login if not manually logged out (checking both state and localStorage)
|
||||||
|
// and not already loading
|
||||||
|
if (!isManuallyLoggedOut && !manuallyLoggedOut && !isLoading) {
|
||||||
|
console.log(
|
||||||
|
"LoginPage: Auto-authenticating because user has not manually logged out",
|
||||||
|
);
|
||||||
|
login().catch((err) => {
|
||||||
|
console.error("FATAL ERROR: Auto-login failed:", err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
"LoginPage: Not auto-authenticating because user manually logged out or is loading",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"isManuallyLoggedOut:",
|
||||||
|
isManuallyLoggedOut,
|
||||||
|
"manuallyLoggedOut:",
|
||||||
|
manuallyLoggedOut,
|
||||||
|
"isLoading:",
|
||||||
|
isLoading,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [login, manuallyLoggedOut, isLoading, isAuthenticated, user]);
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
console.log("LoginPage: Manual login initiated");
|
||||||
|
|
||||||
|
// Reset the manuallyLoggedOut flag in localStorage when user manually clicks login
|
||||||
|
localStorage.setItem("auth_manually_logged_out", "false");
|
||||||
|
console.log(
|
||||||
|
"LoginPage: Reset manuallyLoggedOut to false in localStorage",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store the current path as the return URL after login
|
||||||
|
const returnUrl = window.location.hash.substring(1) || "/";
|
||||||
|
sessionStorage.setItem("oidc_return_url", returnUrl);
|
||||||
|
console.log("LoginPage: Stored return URL:", returnUrl);
|
||||||
|
|
||||||
|
await login();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("FATAL ERROR: Login failed:", err);
|
||||||
|
alert(
|
||||||
|
`Login failed: ${err instanceof Error ? err.message : "Unknown error"}.\n\nCheck the console for more details.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen bg-[#131215]">
|
||||||
|
<div className="w-full max-w-md p-8 space-y-8 bg-[#26242a] rounded-lg shadow-lg">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-2xl font-bold text-white">Merchant Operator</h1>
|
||||||
|
<p className="mt-2 text-gray-400">
|
||||||
|
Sign in with your identity provider
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<button
|
||||||
|
onClick={handleLogin}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full px-4 py-2 text-white bg-[#653cee] rounded-md hover:bg-[#4e2eb8] focus:outline-none focus:ring-2 focus:ring-[#653cee] focus:ring-offset-2 focus:ring-offset-[#26242a] disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white mr-2"></div>
|
||||||
|
<span>Authenticating...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Sign in"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="mt-6 text-sm text-gray-400 text-center">
|
||||||
|
<p>This will redirect you to your configured identity provider.</p>
|
||||||
|
<p className="mt-2">
|
||||||
|
Make sure your OIDC configuration is properly set up.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
19
src/domains/auth/ui/LogoutButton.tsx
Normal file
19
src/domains/auth/ui/LogoutButton.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useAuth } from "../view-model";
|
||||||
|
import Button from "@shared/ui/Button";
|
||||||
|
|
||||||
|
export const LogoutButton: React.FC<{ className?: string }> = ({
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
|
const { isAuthenticated, logout } = useAuth();
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button className={className} variant="outline" onClick={() => logout()}>
|
||||||
|
Log Out
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
62
src/domains/auth/ui/ProtectedRoute.tsx
Normal file
62
src/domains/auth/ui/ProtectedRoute.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React, { ReactNode } from "react";
|
||||||
|
import { useAuth } from "../view-model";
|
||||||
|
import { Navigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
interface ProtectedRouteProps {
|
||||||
|
children: ReactNode;
|
||||||
|
requiredRoles?: string[];
|
||||||
|
redirectTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||||
|
children,
|
||||||
|
requiredRoles = [],
|
||||||
|
redirectTo = "/login",
|
||||||
|
}) => {
|
||||||
|
const { isAuthenticated, isLoading, hasRole } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
console.log("ProtectedRoute: Rendering with state", {
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading,
|
||||||
|
path: location.pathname,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If still loading, redirect to login page which will show the loading state
|
||||||
|
if (isLoading) {
|
||||||
|
console.log("ProtectedRoute: Still loading, redirecting to login page");
|
||||||
|
return <Navigate to={redirectTo} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to login if not authenticated
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
console.log(
|
||||||
|
`User not authenticated, redirecting to ${redirectTo} from ${location.pathname}`,
|
||||||
|
);
|
||||||
|
// Store the current location so we can redirect back after login
|
||||||
|
sessionStorage.setItem("oidc_return_url", location.pathname);
|
||||||
|
return (
|
||||||
|
<Navigate to={redirectTo} state={{ from: location.pathname }} replace />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for required roles if specified
|
||||||
|
if (requiredRoles.length > 0) {
|
||||||
|
const hasRequiredRole = requiredRoles.some((role) => hasRole(role));
|
||||||
|
|
||||||
|
if (!hasRequiredRole) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen">
|
||||||
|
<div className="text-center text-red-500">
|
||||||
|
<div className="text-xl mb-4">Access Denied</div>
|
||||||
|
<div>You don't have permission to access this page.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the protected content
|
||||||
|
console.log("ProtectedRoute: Rendering protected content");
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
4
src/domains/auth/ui/index.ts
Normal file
4
src/domains/auth/ui/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { ProtectedRoute } from "./ProtectedRoute";
|
||||||
|
export { LogoutButton } from "./LogoutButton";
|
||||||
|
export { AuthProvider } from "./AuthProvider";
|
||||||
|
export { LoginPage } from "./LoginPage";
|
44
src/domains/auth/view-model.ts
Normal file
44
src/domains/auth/view-model.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { useUnit } from "effector-react";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { $auth, loginFx, logoutFx } from "./model";
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const { user, isAuthenticated, isLoading, error, manuallyLoggedOut } =
|
||||||
|
useUnit($auth);
|
||||||
|
|
||||||
|
const login = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await loginFx();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const logout = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await logoutFx();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hasRole = useCallback(
|
||||||
|
(role: string) => {
|
||||||
|
return user?.roles.includes(role) || false;
|
||||||
|
},
|
||||||
|
[user],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
manuallyLoggedOut,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
hasRole,
|
||||||
|
};
|
||||||
|
};
|
1
src/domains/dashboard/index.ts
Normal file
1
src/domains/dashboard/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./ui";
|
63
src/domains/dashboard/ui/Dashboard.tsx
Normal file
63
src/domains/dashboard/ui/Dashboard.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useAuth } from "@domains/auth";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@shared/ui";
|
||||||
|
|
||||||
|
export const Dashboard: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Welcome</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p>Hello, {user?.name || "Merchant"}!</p>
|
||||||
|
<p className="mt-2 text-gray-400">
|
||||||
|
Welcome to the Merchant Operator App. Use the sidebar to navigate
|
||||||
|
to different sections.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Quick Stats</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Products</span>
|
||||||
|
<span className="font-medium">12</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Categories</span>
|
||||||
|
<span className="font-medium">4</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Orders</span>
|
||||||
|
<span className="font-medium">24</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Recent Activity</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
<li className="text-gray-400">Product "Espresso" updated</li>
|
||||||
|
<li className="text-gray-400">New category "Beverages" added</li>
|
||||||
|
<li className="text-gray-400">Price for "Cappuccino" changed</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
1
src/domains/dashboard/ui/index.ts
Normal file
1
src/domains/dashboard/ui/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Dashboard } from "./Dashboard";
|
16
src/index.css
Normal file
16
src/index.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Inter font is now loaded from public/fonts/inter.css */
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #131215;
|
||||||
|
color: white;
|
||||||
|
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
color: #adaab7;
|
||||||
|
}
|
29
src/lib/api/merchant/core/ApiError.ts
Normal file
29
src/lib/api/merchant/core/ApiError.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||||
|
import type { ApiResult } from "./ApiResult";
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
public readonly url: string;
|
||||||
|
public readonly status: number;
|
||||||
|
public readonly statusText: string;
|
||||||
|
public readonly body: any;
|
||||||
|
public readonly request: ApiRequestOptions;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
request: ApiRequestOptions,
|
||||||
|
response: ApiResult,
|
||||||
|
message: string,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
this.name = "ApiError";
|
||||||
|
this.url = response.url;
|
||||||
|
this.status = response.status;
|
||||||
|
this.statusText = response.statusText;
|
||||||
|
this.body = response.body;
|
||||||
|
this.request = request;
|
||||||
|
}
|
||||||
|
}
|
24
src/lib/api/merchant/core/ApiRequestOptions.ts
Normal file
24
src/lib/api/merchant/core/ApiRequestOptions.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type ApiRequestOptions = {
|
||||||
|
readonly method:
|
||||||
|
| "GET"
|
||||||
|
| "PUT"
|
||||||
|
| "POST"
|
||||||
|
| "DELETE"
|
||||||
|
| "OPTIONS"
|
||||||
|
| "HEAD"
|
||||||
|
| "PATCH";
|
||||||
|
readonly url: string;
|
||||||
|
readonly path?: Record<string, any>;
|
||||||
|
readonly cookies?: Record<string, any>;
|
||||||
|
readonly headers?: Record<string, any>;
|
||||||
|
readonly query?: Record<string, any>;
|
||||||
|
readonly formData?: Record<string, any>;
|
||||||
|
readonly body?: any;
|
||||||
|
readonly mediaType?: string;
|
||||||
|
readonly responseHeader?: string;
|
||||||
|
readonly errors?: Record<number, string>;
|
||||||
|
};
|
11
src/lib/api/merchant/core/ApiResult.ts
Normal file
11
src/lib/api/merchant/core/ApiResult.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type ApiResult = {
|
||||||
|
readonly url: string;
|
||||||
|
readonly ok: boolean;
|
||||||
|
readonly status: number;
|
||||||
|
readonly statusText: string;
|
||||||
|
readonly body: any;
|
||||||
|
};
|
130
src/lib/api/merchant/core/CancelablePromise.ts
Normal file
130
src/lib/api/merchant/core/CancelablePromise.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export class CancelError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "CancelError";
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isCancelled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnCancel {
|
||||||
|
readonly isResolved: boolean;
|
||||||
|
readonly isRejected: boolean;
|
||||||
|
readonly isCancelled: boolean;
|
||||||
|
|
||||||
|
(cancelHandler: () => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CancelablePromise<T> implements Promise<T> {
|
||||||
|
#isResolved: boolean;
|
||||||
|
#isRejected: boolean;
|
||||||
|
#isCancelled: boolean;
|
||||||
|
readonly #cancelHandlers: (() => void)[];
|
||||||
|
readonly #promise: Promise<T>;
|
||||||
|
#resolve?: (value: T | PromiseLike<T>) => void;
|
||||||
|
#reject?: (reason?: any) => void;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
executor: (
|
||||||
|
resolve: (value: T | PromiseLike<T>) => void,
|
||||||
|
reject: (reason?: any) => void,
|
||||||
|
onCancel: OnCancel,
|
||||||
|
) => void,
|
||||||
|
) {
|
||||||
|
this.#isResolved = false;
|
||||||
|
this.#isRejected = false;
|
||||||
|
this.#isCancelled = false;
|
||||||
|
this.#cancelHandlers = [];
|
||||||
|
this.#promise = new Promise<T>((resolve, reject) => {
|
||||||
|
this.#resolve = resolve;
|
||||||
|
this.#reject = reject;
|
||||||
|
|
||||||
|
const onResolve = (value: T | PromiseLike<T>): void => {
|
||||||
|
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#isResolved = true;
|
||||||
|
if (this.#resolve) this.#resolve(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onReject = (reason?: any): void => {
|
||||||
|
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#isRejected = true;
|
||||||
|
if (this.#reject) this.#reject(reason);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = (cancelHandler: () => void): void => {
|
||||||
|
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#cancelHandlers.push(cancelHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(onCancel, "isResolved", {
|
||||||
|
get: (): boolean => this.#isResolved,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(onCancel, "isRejected", {
|
||||||
|
get: (): boolean => this.#isRejected,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(onCancel, "isCancelled", {
|
||||||
|
get: (): boolean => this.#isCancelled,
|
||||||
|
});
|
||||||
|
|
||||||
|
return executor(onResolve, onReject, onCancel as OnCancel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get [Symbol.toStringTag]() {
|
||||||
|
return "Cancellable Promise";
|
||||||
|
}
|
||||||
|
|
||||||
|
public then<TResult1 = T, TResult2 = never>(
|
||||||
|
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||||
|
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
|
||||||
|
): Promise<TResult1 | TResult2> {
|
||||||
|
return this.#promise.then(onFulfilled, onRejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public catch<TResult = never>(
|
||||||
|
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null,
|
||||||
|
): Promise<T | TResult> {
|
||||||
|
return this.#promise.catch(onRejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
public finally(onFinally?: (() => void) | null): Promise<T> {
|
||||||
|
return this.#promise.finally(onFinally);
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancel(): void {
|
||||||
|
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#isCancelled = true;
|
||||||
|
if (this.#cancelHandlers.length) {
|
||||||
|
try {
|
||||||
|
for (const cancelHandler of this.#cancelHandlers) {
|
||||||
|
cancelHandler();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Cancellation threw an error", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#cancelHandlers.length = 0;
|
||||||
|
if (this.#reject) this.#reject(new CancelError("Request aborted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isCancelled(): boolean {
|
||||||
|
return this.#isCancelled;
|
||||||
|
}
|
||||||
|
}
|
32
src/lib/api/merchant/core/OpenAPI.ts
Normal file
32
src/lib/api/merchant/core/OpenAPI.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||||
|
|
||||||
|
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||||
|
type Headers = Record<string, string>;
|
||||||
|
|
||||||
|
export type OpenAPIConfig = {
|
||||||
|
BASE: string;
|
||||||
|
VERSION: string;
|
||||||
|
WITH_CREDENTIALS: boolean;
|
||||||
|
CREDENTIALS: "include" | "omit" | "same-origin";
|
||||||
|
TOKEN?: string | Resolver<string> | undefined;
|
||||||
|
USERNAME?: string | Resolver<string> | undefined;
|
||||||
|
PASSWORD?: string | Resolver<string> | undefined;
|
||||||
|
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||||
|
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OpenAPI: OpenAPIConfig = {
|
||||||
|
BASE: "",
|
||||||
|
VERSION: "0.0.0",
|
||||||
|
WITH_CREDENTIALS: false,
|
||||||
|
CREDENTIALS: "include",
|
||||||
|
TOKEN: undefined,
|
||||||
|
USERNAME: undefined,
|
||||||
|
PASSWORD: undefined,
|
||||||
|
HEADERS: undefined,
|
||||||
|
ENCODE_PATH: undefined,
|
||||||
|
};
|
38
src/lib/api/merchant/core/initialize.ts
Normal file
38
src/lib/api/merchant/core/initialize.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Initialize OpenAPI configuration
|
||||||
|
import { OpenAPI } from "./OpenAPI";
|
||||||
|
import { getApiUrlSingleton } from "@shared/lib/api-url";
|
||||||
|
|
||||||
|
// Add a global debug function for consistent logging
|
||||||
|
const debug = (message: string) => {
|
||||||
|
console.log(`[OPENAPI-INIT] ${message}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the OpenAPI configuration with the API URL
|
||||||
|
*/
|
||||||
|
export const initializeOpenAPI = () => {
|
||||||
|
try {
|
||||||
|
debug("Initializing OpenAPI configuration");
|
||||||
|
|
||||||
|
// Get the API URL using our utility function
|
||||||
|
const apiUrl = getApiUrlSingleton();
|
||||||
|
|
||||||
|
// Set the OpenAPI base URL
|
||||||
|
OpenAPI.BASE = apiUrl;
|
||||||
|
debug(`OpenAPI initialized with BASE URL: ${OpenAPI.BASE}`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("FATAL ERROR: Failed to initialize OpenAPI:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a debug statement to confirm the script is being executed
|
||||||
|
debug("initialize.ts script is being executed");
|
||||||
|
|
||||||
|
// Call initialization function
|
||||||
|
const initialized = initializeOpenAPI();
|
||||||
|
|
||||||
|
// Log initialization status
|
||||||
|
debug(`OpenAPI initialization ${initialized ? "successful" : "failed"}`);
|
369
src/lib/api/merchant/core/request.ts
Normal file
369
src/lib/api/merchant/core/request.ts
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import axios from "axios";
|
||||||
|
import type {
|
||||||
|
AxiosError,
|
||||||
|
AxiosRequestConfig,
|
||||||
|
AxiosResponse,
|
||||||
|
AxiosInstance,
|
||||||
|
} from "axios";
|
||||||
|
import FormData from "form-data";
|
||||||
|
|
||||||
|
import { ApiError } from "./ApiError";
|
||||||
|
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||||
|
import type { ApiResult } from "./ApiResult";
|
||||||
|
import { CancelablePromise } from "./CancelablePromise";
|
||||||
|
import type { OnCancel } from "./CancelablePromise";
|
||||||
|
import type { OpenAPIConfig } from "./OpenAPI";
|
||||||
|
|
||||||
|
export const isDefined = <T>(
|
||||||
|
value: T | null | undefined,
|
||||||
|
): value is Exclude<T, null | undefined> => {
|
||||||
|
return value !== undefined && value !== null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isString = (value: any): value is string => {
|
||||||
|
return typeof value === "string";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isStringWithValue = (value: any): value is string => {
|
||||||
|
return isString(value) && value !== "";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isBlob = (value: any): value is Blob => {
|
||||||
|
return (
|
||||||
|
typeof value === "object" &&
|
||||||
|
typeof value.type === "string" &&
|
||||||
|
typeof value.stream === "function" &&
|
||||||
|
typeof value.arrayBuffer === "function" &&
|
||||||
|
typeof value.constructor === "function" &&
|
||||||
|
typeof value.constructor.name === "string" &&
|
||||||
|
/^(Blob|File)$/.test(value.constructor.name) &&
|
||||||
|
/^(Blob|File)$/.test(value[Symbol.toStringTag])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isFormData = (value: any): value is FormData => {
|
||||||
|
return value instanceof FormData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isSuccess = (status: number): boolean => {
|
||||||
|
return status >= 200 && status < 300;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const base64 = (str: string): string => {
|
||||||
|
try {
|
||||||
|
return btoa(str);
|
||||||
|
} catch (err) {
|
||||||
|
// @ts-ignore
|
||||||
|
return Buffer.from(str).toString("base64");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQueryString = (params: Record<string, any>): string => {
|
||||||
|
const qs: string[] = [];
|
||||||
|
|
||||||
|
const append = (key: string, value: any) => {
|
||||||
|
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const process = (key: string, value: any) => {
|
||||||
|
if (isDefined(value)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v) => {
|
||||||
|
process(key, v);
|
||||||
|
});
|
||||||
|
} else if (typeof value === "object") {
|
||||||
|
Object.entries(value).forEach(([k, v]) => {
|
||||||
|
process(`${key}[${k}]`, v);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
process(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (qs.length > 0) {
|
||||||
|
return `?${qs.join("&")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||||
|
const encoder = config.ENCODE_PATH || encodeURI;
|
||||||
|
|
||||||
|
const path = options.url
|
||||||
|
.replace("{api-version}", config.VERSION)
|
||||||
|
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
||||||
|
if (options.path?.hasOwnProperty(group)) {
|
||||||
|
return encoder(String(options.path[group]));
|
||||||
|
}
|
||||||
|
return substring;
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `${config.BASE}${path}`;
|
||||||
|
if (options.query) {
|
||||||
|
return `${url}${getQueryString(options.query)}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFormData = (
|
||||||
|
options: ApiRequestOptions,
|
||||||
|
): FormData | undefined => {
|
||||||
|
if (options.formData) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
const process = (key: string, value: any) => {
|
||||||
|
if (isString(value) || isBlob(value)) {
|
||||||
|
formData.append(key, value);
|
||||||
|
} else {
|
||||||
|
formData.append(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(options.formData)
|
||||||
|
.filter(([_, value]) => isDefined(value))
|
||||||
|
.forEach(([key, value]) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v) => process(key, v));
|
||||||
|
} else {
|
||||||
|
process(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||||
|
|
||||||
|
export const resolve = async <T>(
|
||||||
|
options: ApiRequestOptions,
|
||||||
|
resolver?: T | Resolver<T>,
|
||||||
|
): Promise<T | undefined> => {
|
||||||
|
if (typeof resolver === "function") {
|
||||||
|
return (resolver as Resolver<T>)(options);
|
||||||
|
}
|
||||||
|
return resolver;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHeaders = async (
|
||||||
|
config: OpenAPIConfig,
|
||||||
|
options: ApiRequestOptions,
|
||||||
|
formData?: FormData,
|
||||||
|
): Promise<Record<string, string>> => {
|
||||||
|
const [token, username, password, additionalHeaders] = await Promise.all([
|
||||||
|
resolve(options, config.TOKEN),
|
||||||
|
resolve(options, config.USERNAME),
|
||||||
|
resolve(options, config.PASSWORD),
|
||||||
|
resolve(options, config.HEADERS),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const formHeaders =
|
||||||
|
(typeof formData?.getHeaders === "function" && formData?.getHeaders()) ||
|
||||||
|
{};
|
||||||
|
|
||||||
|
const headers = Object.entries({
|
||||||
|
Accept: "application/json",
|
||||||
|
...additionalHeaders,
|
||||||
|
...options.headers,
|
||||||
|
...formHeaders,
|
||||||
|
})
|
||||||
|
.filter(([_, value]) => isDefined(value))
|
||||||
|
.reduce(
|
||||||
|
(headers, [key, value]) => ({
|
||||||
|
...headers,
|
||||||
|
[key]: String(value),
|
||||||
|
}),
|
||||||
|
{} as Record<string, string>,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isStringWithValue(token)) {
|
||||||
|
headers["Authorization"] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||||
|
const credentials = base64(`${username}:${password}`);
|
||||||
|
headers["Authorization"] = `Basic ${credentials}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.body !== undefined) {
|
||||||
|
if (options.mediaType) {
|
||||||
|
headers["Content-Type"] = options.mediaType;
|
||||||
|
} else if (isBlob(options.body)) {
|
||||||
|
headers["Content-Type"] = options.body.type || "application/octet-stream";
|
||||||
|
} else if (isString(options.body)) {
|
||||||
|
headers["Content-Type"] = "text/plain";
|
||||||
|
} else if (!isFormData(options.body)) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRequestBody = (options: ApiRequestOptions): any => {
|
||||||
|
if (options.body) {
|
||||||
|
return options.body;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendRequest = async <T>(
|
||||||
|
config: OpenAPIConfig,
|
||||||
|
options: ApiRequestOptions,
|
||||||
|
url: string,
|
||||||
|
body: any,
|
||||||
|
formData: FormData | undefined,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
onCancel: OnCancel,
|
||||||
|
axiosClient: AxiosInstance,
|
||||||
|
): Promise<AxiosResponse<T>> => {
|
||||||
|
const source = axios.CancelToken.source();
|
||||||
|
|
||||||
|
const requestConfig: AxiosRequestConfig = {
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
data: body ?? formData,
|
||||||
|
method: options.method,
|
||||||
|
withCredentials: config.WITH_CREDENTIALS,
|
||||||
|
withXSRFToken:
|
||||||
|
config.CREDENTIALS === "include" ? config.WITH_CREDENTIALS : false,
|
||||||
|
cancelToken: source.token,
|
||||||
|
};
|
||||||
|
|
||||||
|
onCancel(() => source.cancel("The user aborted a request."));
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await axiosClient.request(requestConfig);
|
||||||
|
} catch (error) {
|
||||||
|
const axiosError = error as AxiosError<T>;
|
||||||
|
if (axiosError.response) {
|
||||||
|
return axiosError.response;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getResponseHeader = (
|
||||||
|
response: AxiosResponse<any>,
|
||||||
|
responseHeader?: string,
|
||||||
|
): string | undefined => {
|
||||||
|
if (responseHeader) {
|
||||||
|
const content = response.headers[responseHeader];
|
||||||
|
if (isString(content)) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getResponseBody = (response: AxiosResponse<any>): any => {
|
||||||
|
if (response.status !== 204) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const catchErrorCodes = (
|
||||||
|
options: ApiRequestOptions,
|
||||||
|
result: ApiResult,
|
||||||
|
): void => {
|
||||||
|
const errors: Record<number, string> = {
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Not Found",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
502: "Bad Gateway",
|
||||||
|
503: "Service Unavailable",
|
||||||
|
...options.errors,
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = errors[result.status];
|
||||||
|
if (error) {
|
||||||
|
throw new ApiError(options, result, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
const errorStatus = result.status ?? "unknown";
|
||||||
|
const errorStatusText = result.statusText ?? "unknown";
|
||||||
|
const errorBody = (() => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(result.body, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
throw new ApiError(
|
||||||
|
options,
|
||||||
|
result,
|
||||||
|
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request method
|
||||||
|
* @param config The OpenAPI configuration object
|
||||||
|
* @param options The request options from the service
|
||||||
|
* @param axiosClient The axios client instance to use
|
||||||
|
* @returns CancelablePromise<T>
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
export const request = <T>(
|
||||||
|
config: OpenAPIConfig,
|
||||||
|
options: ApiRequestOptions,
|
||||||
|
axiosClient: AxiosInstance = axios,
|
||||||
|
): CancelablePromise<T> => {
|
||||||
|
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
||||||
|
try {
|
||||||
|
const url = getUrl(config, options);
|
||||||
|
const formData = getFormData(options);
|
||||||
|
const body = getRequestBody(options);
|
||||||
|
const headers = await getHeaders(config, options, formData);
|
||||||
|
|
||||||
|
if (!onCancel.isCancelled) {
|
||||||
|
const response = await sendRequest<T>(
|
||||||
|
config,
|
||||||
|
options,
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
formData,
|
||||||
|
headers,
|
||||||
|
onCancel,
|
||||||
|
axiosClient,
|
||||||
|
);
|
||||||
|
const responseBody = getResponseBody(response);
|
||||||
|
const responseHeader = getResponseHeader(
|
||||||
|
response,
|
||||||
|
options.responseHeader,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result: ApiResult = {
|
||||||
|
url,
|
||||||
|
ok: isSuccess(response.status),
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
body: responseHeader ?? responseBody,
|
||||||
|
};
|
||||||
|
|
||||||
|
catchErrorCodes(options, result);
|
||||||
|
|
||||||
|
resolve(result.body);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
174
src/lib/api/merchant/index.ts
Normal file
174
src/lib/api/merchant/index.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export { ApiError } from "./core/ApiError";
|
||||||
|
export { CancelablePromise, CancelError } from "./core/CancelablePromise";
|
||||||
|
export { OpenAPI } from "./core/OpenAPI";
|
||||||
|
export type { OpenAPIConfig } from "./core/OpenAPI";
|
||||||
|
|
||||||
|
export type { _void } from "./models/_void";
|
||||||
|
export type { addressDto } from "./models/addressDto";
|
||||||
|
export { addressType } from "./models/addressType";
|
||||||
|
export { aspectType } from "./models/aspectType";
|
||||||
|
export type { assetCollectionDto } from "./models/assetCollectionDto";
|
||||||
|
export type { authDocumentDto } from "./models/authDocumentDto";
|
||||||
|
export type { authenticationResultDto } from "./models/authenticationResultDto";
|
||||||
|
export type { authHeaderResponseDto } from "./models/authHeaderResponseDto";
|
||||||
|
export type { baseCreateSellerRevisionDto } from "./models/baseCreateSellerRevisionDto";
|
||||||
|
export type { baseOfferDto } from "./models/baseOfferDto";
|
||||||
|
export type { baseTriggerDto } from "./models/baseTriggerDto";
|
||||||
|
export type { basketDto } from "./models/basketDto";
|
||||||
|
export type { bundleDiscountOfferDto } from "./models/bundleDiscountOfferDto";
|
||||||
|
export type { bundleTriggerDto } from "./models/bundleTriggerDto";
|
||||||
|
export type { calculatedCheckoutDto } from "./models/calculatedCheckoutDto";
|
||||||
|
export type { calculatedProductDto } from "./models/calculatedProductDto";
|
||||||
|
export type { categoryCompositeDto } from "./models/categoryCompositeDto";
|
||||||
|
export type { categoryDto } from "./models/categoryDto";
|
||||||
|
export type { categoryQueryRequestDto } from "./models/categoryQueryRequestDto";
|
||||||
|
export type { categoryRevisionDto } from "./models/categoryRevisionDto";
|
||||||
|
export type { categoryRevisionQueryRequestDto } from "./models/categoryRevisionQueryRequestDto";
|
||||||
|
export type { categoryStopListDto } from "./models/categoryStopListDto";
|
||||||
|
export type { categoryViewDto } from "./models/categoryViewDto";
|
||||||
|
export type { checkoutOptionsDto } from "./models/checkoutOptionsDto";
|
||||||
|
export { commonOrderFailureResult } from "./models/commonOrderFailureResult";
|
||||||
|
export { commonOrderStatus } from "./models/commonOrderStatus";
|
||||||
|
export type { couponTriggerDto } from "./models/couponTriggerDto";
|
||||||
|
export type { courierInfoDto } from "./models/courierInfoDto";
|
||||||
|
export type { createCategoryDto } from "./models/createCategoryDto";
|
||||||
|
export type { createCategoryRevisionDto } from "./models/createCategoryRevisionDto";
|
||||||
|
export type { createOptionsRevisionDto } from "./models/createOptionsRevisionDto";
|
||||||
|
export type { createProductDto } from "./models/createProductDto";
|
||||||
|
export type { createProductRevisionDto } from "./models/createProductRevisionDto";
|
||||||
|
export type { createPromotionDto } from "./models/createPromotionDto";
|
||||||
|
export type { createRestaurantDto } from "./models/createRestaurantDto";
|
||||||
|
export type { createRestaurantRevisionDto } from "./models/createRestaurantRevisionDto";
|
||||||
|
export type { createSellerDto } from "./models/createSellerDto";
|
||||||
|
export type { createSellerRevisionDto } from "./models/createSellerRevisionDto";
|
||||||
|
export type { createTextRevisionDto } from "./models/createTextRevisionDto";
|
||||||
|
export type { customerGroupTriggerDto } from "./models/customerGroupTriggerDto";
|
||||||
|
export type { customerHistoryTriggerDto } from "./models/customerHistoryTriggerDto";
|
||||||
|
export type { customerLoyaltyTriggerDto } from "./models/customerLoyaltyTriggerDto";
|
||||||
|
export type { deliveryAddressDto } from "./models/deliveryAddressDto";
|
||||||
|
export { deliveryFailureResult } from "./models/deliveryFailureResult";
|
||||||
|
export type { deliveryLocationDto } from "./models/deliveryLocationDto";
|
||||||
|
export { deliveryOrderStatus } from "./models/deliveryOrderStatus";
|
||||||
|
export type { deliveryStateDto } from "./models/deliveryStateDto";
|
||||||
|
export type { digitalEngagementTriggerDto } from "./models/digitalEngagementTriggerDto";
|
||||||
|
export type { discountProductOfferDto } from "./models/discountProductOfferDto";
|
||||||
|
export type { discountTotalOfferDto } from "./models/discountTotalOfferDto";
|
||||||
|
export { discountType } from "./models/discountType";
|
||||||
|
export { dispatchMethodType } from "./models/dispatchMethodType";
|
||||||
|
export type { freeDeliveryOfferDto } from "./models/freeDeliveryOfferDto";
|
||||||
|
export type { freeItemOfferDto } from "./models/freeItemOfferDto";
|
||||||
|
export type { gpsLocationDto } from "./models/gpsLocationDto";
|
||||||
|
export type { holidayTriggerDto } from "./models/holidayTriggerDto";
|
||||||
|
export type { humanVerificationRequestDto } from "./models/humanVerificationRequestDto";
|
||||||
|
export type { humanVerificationStatusDto } from "./models/humanVerificationStatusDto";
|
||||||
|
export type { imageReferenceDto } from "./models/imageReferenceDto";
|
||||||
|
export type { iTriggerDto } from "./models/iTriggerDto";
|
||||||
|
export type { localizedTextDto } from "./models/localizedTextDto";
|
||||||
|
export type { managerOverrideTriggerDto } from "./models/managerOverrideTriggerDto";
|
||||||
|
export type { oidcConnectResponseDto } from "./models/oidcConnectResponseDto";
|
||||||
|
export type { optionDto } from "./models/optionDto";
|
||||||
|
export type { optionsQueryDto } from "./models/optionsQueryDto";
|
||||||
|
export type { optionsRevisionDto } from "./models/optionsRevisionDto";
|
||||||
|
export type { optionsViewDto } from "./models/optionsViewDto";
|
||||||
|
export type { orderCreateDto } from "./models/orderCreateDto";
|
||||||
|
export type { orderDto } from "./models/orderDto";
|
||||||
|
export type { orderFailureRequestDto } from "./models/orderFailureRequestDto";
|
||||||
|
export type { orderNextStatusDto } from "./models/orderNextStatusDto";
|
||||||
|
export type { orderQueryRequestDto } from "./models/orderQueryRequestDto";
|
||||||
|
export type { orderStateChangeRequestDto } from "./models/orderStateChangeRequestDto";
|
||||||
|
export type { orderStateDto } from "./models/orderStateDto";
|
||||||
|
export { orderTriggerDto } from "./models/orderTriggerDto";
|
||||||
|
export type { orderViewDto } from "./models/orderViewDto";
|
||||||
|
export { paymentType } from "./models/paymentType";
|
||||||
|
export { phoneVerificationState } from "./models/phoneVerificationState";
|
||||||
|
export type { preparationStateDto } from "./models/preparationStateDto";
|
||||||
|
export type { priceEstimationDto } from "./models/priceEstimationDto";
|
||||||
|
export type { priceEstimationRequestDto } from "./models/priceEstimationRequestDto";
|
||||||
|
export type { problemDetails } from "./models/problemDetails";
|
||||||
|
export type { productCompositeDto } from "./models/productCompositeDto";
|
||||||
|
export type { productDto } from "./models/productDto";
|
||||||
|
export type { productQueryRequestDto } from "./models/productQueryRequestDto";
|
||||||
|
export type { productRevisionDto } from "./models/productRevisionDto";
|
||||||
|
export type { productRevisionQueryRequestDto } from "./models/productRevisionQueryRequestDto";
|
||||||
|
export type { productStopListDto } from "./models/productStopListDto";
|
||||||
|
export type { productUnavailableDto } from "./models/productUnavailableDto";
|
||||||
|
export { productUnavailableReason } from "./models/productUnavailableReason";
|
||||||
|
export type { promotionQueryRequestDto } from "./models/promotionQueryRequestDto";
|
||||||
|
export type { promotionViewDto } from "./models/promotionViewDto";
|
||||||
|
export type { purchaseTriggerDto } from "./models/purchaseTriggerDto";
|
||||||
|
export type { QueryResultDto_CategoryDto_ } from "./models/QueryResultDto_CategoryDto_";
|
||||||
|
export type { QueryResultDto_CategoryRevisionDto_ } from "./models/QueryResultDto_CategoryRevisionDto_";
|
||||||
|
export type { QueryResultDto_OptionsRevisionDto_ } from "./models/QueryResultDto_OptionsRevisionDto_";
|
||||||
|
export type { QueryResultDto_OrderViewDto_ } from "./models/QueryResultDto_OrderViewDto_";
|
||||||
|
export type { QueryResultDto_ProductRevisionDto_ } from "./models/QueryResultDto_ProductRevisionDto_";
|
||||||
|
export type { QueryResultDto_PromotionViewDto_ } from "./models/QueryResultDto_PromotionViewDto_";
|
||||||
|
export type { QueryResultDto_SellerDto_ } from "./models/QueryResultDto_SellerDto_";
|
||||||
|
export type { QueryResultDto_SellerRevisionDto_ } from "./models/QueryResultDto_SellerRevisionDto_";
|
||||||
|
export type { QueryResultDto_TextRevisionDto_ } from "./models/QueryResultDto_TextRevisionDto_";
|
||||||
|
export type { QueryResultDto_UserDto_ } from "./models/QueryResultDto_UserDto_";
|
||||||
|
export type { restaurantDto } from "./models/restaurantDto";
|
||||||
|
export type { restaurantRevisionDto } from "./models/restaurantRevisionDto";
|
||||||
|
export type { rolePermissionsDto } from "./models/rolePermissionsDto";
|
||||||
|
export type { scheduleDto } from "./models/scheduleDto";
|
||||||
|
export type { scheduleExceptionDto } from "./models/scheduleExceptionDto";
|
||||||
|
export type { selectedOptionDto } from "./models/selectedOptionDto";
|
||||||
|
export type { selectedProductDto } from "./models/selectedProductDto";
|
||||||
|
export type { sellerCompositeDto } from "./models/sellerCompositeDto";
|
||||||
|
export type { sellerDto } from "./models/sellerDto";
|
||||||
|
export type { sellerOperationalStateDto } from "./models/sellerOperationalStateDto";
|
||||||
|
export type { sellerOperationalStateTriggerDto } from "./models/sellerOperationalStateTriggerDto";
|
||||||
|
export type { sellerPublicAggregateFullDto } from "./models/sellerPublicAggregateFullDto";
|
||||||
|
export type { sellerQueryRequestDto } from "./models/sellerQueryRequestDto";
|
||||||
|
export type { sellerRevisionDto } from "./models/sellerRevisionDto";
|
||||||
|
export type { sellerRevisionQueryRequestDto } from "./models/sellerRevisionQueryRequestDto";
|
||||||
|
export type { sellerViewDto } from "./models/sellerViewDto";
|
||||||
|
export type { sessionIpResponseDto } from "./models/sessionIpResponseDto";
|
||||||
|
export { sessionStatus } from "./models/sessionStatus";
|
||||||
|
export type { sessionStatusDto } from "./models/sessionStatusDto";
|
||||||
|
export type { signedAuthDocumentDto } from "./models/signedAuthDocumentDto";
|
||||||
|
export type { simpleContactDto } from "./models/simpleContactDto";
|
||||||
|
export type { startSessionResponseDto } from "./models/startSessionResponseDto";
|
||||||
|
export type { stopListTriggerDto } from "./models/stopListTriggerDto";
|
||||||
|
export type { systemSecurityDto } from "./models/systemSecurityDto";
|
||||||
|
export type { tag } from "./models/tag";
|
||||||
|
export type { textQueryRequestDto } from "./models/textQueryRequestDto";
|
||||||
|
export type { textRevisionDto } from "./models/textRevisionDto";
|
||||||
|
export type { timeRangeDto } from "./models/timeRangeDto";
|
||||||
|
export type { timeTriggerDto } from "./models/timeTriggerDto";
|
||||||
|
export { triggerEffect } from "./models/triggerEffect";
|
||||||
|
export type { updatePermissionsRequest } from "./models/updatePermissionsRequest";
|
||||||
|
export type { userDto } from "./models/userDto";
|
||||||
|
export type { userQueryRequest } from "./models/userQueryRequest";
|
||||||
|
export type { verifiedValueDto } from "./models/verifiedValueDto";
|
||||||
|
export { versionCheck } from "./models/versionCheck";
|
||||||
|
|
||||||
|
export { AiService } from "./services/AiService";
|
||||||
|
export { AuthenticationService } from "./services/AuthenticationService";
|
||||||
|
export { CategoriesCanonicalService } from "./services/CategoriesCanonicalService";
|
||||||
|
export { CategoriesRevisionsService } from "./services/CategoriesRevisionsService";
|
||||||
|
export { CategoriesViewsService } from "./services/CategoriesViewsService";
|
||||||
|
export { DeliveriesService } from "./services/DeliveriesService";
|
||||||
|
export { ImagesService } from "./services/ImagesService";
|
||||||
|
export { OidcService } from "./services/OidcService";
|
||||||
|
export { OrdersService } from "./services/OrdersService";
|
||||||
|
export { OrdersBasketService } from "./services/OrdersBasketService";
|
||||||
|
export { ProductOptionsRevisionsService } from "./services/ProductOptionsRevisionsService";
|
||||||
|
export { ProductsCanonicalService } from "./services/ProductsCanonicalService";
|
||||||
|
export { ProductsRevisionsService } from "./services/ProductsRevisionsService";
|
||||||
|
export { ProductsViewsService } from "./services/ProductsViewsService";
|
||||||
|
export { PromotionsService } from "./services/PromotionsService";
|
||||||
|
export { SecurityService } from "./services/SecurityService";
|
||||||
|
export { SellersCanonicalService } from "./services/SellersCanonicalService";
|
||||||
|
export { SellersRevisionsService } from "./services/SellersRevisionsService";
|
||||||
|
export { SellersViewsService } from "./services/SellersViewsService";
|
||||||
|
export { ServerSideEventsService } from "./services/ServerSideEventsService";
|
||||||
|
export { SystemService } from "./services/SystemService";
|
||||||
|
export { TestsService } from "./services/TestsService";
|
||||||
|
export { TranslatableTextRevisionsService } from "./services/TranslatableTextRevisionsService";
|
||||||
|
export { UserService } from "./services/UserService";
|
||||||
|
export { VerificationsService } from "./services/VerificationsService";
|
||||||
|
export { WebService } from "./services/WebService";
|
13
src/lib/api/merchant/models/QueryResultDto_CategoryDto_.ts
Normal file
13
src/lib/api/merchant/models/QueryResultDto_CategoryDto_.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { categoryDto } from "./categoryDto";
|
||||||
|
/**
|
||||||
|
* The UTC server time is included in the response to allow the client to know (by the server's clock) when the query was performed.
|
||||||
|
* It is important to populate this field with the server's time at the beginning of the query execution.
|
||||||
|
*/
|
||||||
|
export type QueryResultDto_CategoryDto_ = {
|
||||||
|
serverTimeUtc?: string;
|
||||||
|
results?: Array<categoryDto>;
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { categoryRevisionDto } from "./categoryRevisionDto";
|
||||||
|
/**
|
||||||
|
* The UTC server time is included in the response to allow the client to know (by the server's clock) when the query was performed.
|
||||||
|
* It is important to populate this field with the server's time at the beginning of the query execution.
|
||||||
|
*/
|
||||||
|
export type QueryResultDto_CategoryRevisionDto_ = {
|
||||||
|
serverTimeUtc?: string;
|
||||||
|
results?: Array<categoryRevisionDto>;
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { optionsRevisionDto } from "./optionsRevisionDto";
|
||||||
|
/**
|
||||||
|
* The UTC server time is included in the response to allow the client to know (by the server's clock) when the query was performed.
|
||||||
|
* It is important to populate this field with the server's time at the beginning of the query execution.
|
||||||
|
*/
|
||||||
|
export type QueryResultDto_OptionsRevisionDto_ = {
|
||||||
|
serverTimeUtc?: string;
|
||||||
|
results?: Array<optionsRevisionDto>;
|
||||||
|
};
|
13
src/lib/api/merchant/models/QueryResultDto_OrderViewDto_.ts
Normal file
13
src/lib/api/merchant/models/QueryResultDto_OrderViewDto_.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { orderViewDto } from "./orderViewDto";
|
||||||
|
/**
|
||||||
|
* The UTC server time is included in the response to allow the client to know (by the server's clock) when the query was performed.
|
||||||
|
* It is important to populate this field with the server's time at the beginning of the query execution.
|
||||||
|
*/
|
||||||
|
export type QueryResultDto_OrderViewDto_ = {
|
||||||
|
serverTimeUtc?: string;
|
||||||
|
results?: Array<orderViewDto>;
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { productRevisionDto } from "./productRevisionDto";
|
||||||
|
/**
|
||||||
|
* The UTC server time is included in the response to allow the client to know (by the server's clock) when the query was performed.
|
||||||
|
* It is important to populate this field with the server's time at the beginning of the query execution.
|
||||||
|
*/
|
||||||
|
export type QueryResultDto_ProductRevisionDto_ = {
|
||||||
|
serverTimeUtc?: string;
|
||||||
|
results?: Array<productRevisionDto>;
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { promotionViewDto } from "./promotionViewDto";
|
||||||
|
/**
|
||||||
|
* The UTC server time is included in the response to allow the client to know (by the server's clock) when the query was performed.
|
||||||
|
* It is important to populate this field with the server's time at the beginning of the query execution.
|
||||||
|
*/
|
||||||
|
export type QueryResultDto_PromotionViewDto_ = {
|
||||||
|
serverTimeUtc?: string;
|
||||||
|
results?: Array<promotionViewDto>;
|
||||||
|
};
|
13
src/lib/api/merchant/models/QueryResultDto_SellerDto_.ts
Normal file
13
src/lib/api/merchant/models/QueryResultDto_SellerDto_.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { sellerDto } from "./sellerDto";
|
||||||
|
/**
|
||||||
|
* The UTC server time is included in the response to allow the client to know (by the server's clock) when the query was performed.
|
||||||
|
* It is important to populate this field with the server's time at the beginning of the query execution.
|
||||||
|
*/
|
||||||
|
export type QueryResultDto_SellerDto_ = {
|
||||||
|
serverTimeUtc?: string;
|
||||||
|
results?: Array<sellerDto>;
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { sellerRevisionDto } from "./sellerRevisionDto";
|
||||||
|
/**
|
||||||
|
* The UTC server time is included in the response to allow the client to know (by the server's clock) when the query was performed.
|
||||||
|
* It is important to populate this field with the server's time at the beginning of the query execution.
|
||||||
|
*/
|
||||||
|
export type QueryResultDto_SellerRevisionDto_ = {
|
||||||
|
serverTimeUtc?: string;
|
||||||
|
results?: Array<sellerRevisionDto>;
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { textRevisionDto } from "./textRevisionDto";
|
||||||
|
/**
|
||||||
|
* The UTC server time is included in the response to allow the client to know (by the server's clock) when the query was performed.
|
||||||
|
* It is important to populate this field with the server's time at the beginning of the query execution.
|
||||||
|
*/
|
||||||
|
export type QueryResultDto_TextRevisionDto_ = {
|
||||||
|
serverTimeUtc?: string;
|
||||||
|
results?: Array<textRevisionDto>;
|
||||||
|
};
|
13
src/lib/api/merchant/models/QueryResultDto_UserDto_.ts
Normal file
13
src/lib/api/merchant/models/QueryResultDto_UserDto_.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { userDto } from "./userDto";
|
||||||
|
/**
|
||||||
|
* The UTC server time is included in the response to allow the client to know (by the server's clock) when the query was performed.
|
||||||
|
* It is important to populate this field with the server's time at the beginning of the query execution.
|
||||||
|
*/
|
||||||
|
export type QueryResultDto_UserDto_ = {
|
||||||
|
serverTimeUtc?: string;
|
||||||
|
results?: Array<userDto>;
|
||||||
|
};
|
5
src/lib/api/merchant/models/_void.ts
Normal file
5
src/lib/api/merchant/models/_void.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type _void = Record<string, any>;
|
44
src/lib/api/merchant/models/addressDto.ts
Normal file
44
src/lib/api/merchant/models/addressDto.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { addressType } from "./addressType";
|
||||||
|
export type addressDto = {
|
||||||
|
/**
|
||||||
|
* Primary address line; required and defaults to an empty string.
|
||||||
|
*/
|
||||||
|
line1?: string | null;
|
||||||
|
/**
|
||||||
|
* Optional second address line for additional info.
|
||||||
|
*/
|
||||||
|
line2?: string | null;
|
||||||
|
/**
|
||||||
|
* Optional third address line.
|
||||||
|
*/
|
||||||
|
line3?: string | null;
|
||||||
|
/**
|
||||||
|
* Optional floor information (e.g., "3", "roof", "ground floor").
|
||||||
|
*/
|
||||||
|
floor?: string | null;
|
||||||
|
/**
|
||||||
|
* Optional flat or apartment number.
|
||||||
|
*/
|
||||||
|
flat?: string | null;
|
||||||
|
/**
|
||||||
|
* Optional building identifier (name or number).
|
||||||
|
*/
|
||||||
|
building?: string | null;
|
||||||
|
/**
|
||||||
|
* Optional comments for extra address details.
|
||||||
|
*/
|
||||||
|
comment?: string | null;
|
||||||
|
/**
|
||||||
|
* Optional postal code.
|
||||||
|
*/
|
||||||
|
postCode?: string | null;
|
||||||
|
/**
|
||||||
|
* Optional region (e.g., state or province).
|
||||||
|
*/
|
||||||
|
region?: string | null;
|
||||||
|
type?: addressType;
|
||||||
|
};
|
36
src/lib/api/merchant/models/addressType.ts
Normal file
36
src/lib/api/merchant/models/addressType.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* **Enum Values:**
|
||||||
|
*
|
||||||
|
* - 0 - **None**
|
||||||
|
* - 1 - **House**
|
||||||
|
* - 2 - **Apartment**
|
||||||
|
* - 3 - **Office**
|
||||||
|
* - 4 - **Other**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export enum addressType {
|
||||||
|
/**
|
||||||
|
* (value: 0)
|
||||||
|
*/
|
||||||
|
None = 0,
|
||||||
|
/**
|
||||||
|
* (value: 1)
|
||||||
|
*/
|
||||||
|
House = 1,
|
||||||
|
/**
|
||||||
|
* (value: 2)
|
||||||
|
*/
|
||||||
|
Apartment = 2,
|
||||||
|
/**
|
||||||
|
* (value: 3)
|
||||||
|
*/
|
||||||
|
Office = 3,
|
||||||
|
/**
|
||||||
|
* (value: 4)
|
||||||
|
*/
|
||||||
|
Other = 4,
|
||||||
|
}
|
31
src/lib/api/merchant/models/aspectType.ts
Normal file
31
src/lib/api/merchant/models/aspectType.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* **Enum Values:**
|
||||||
|
*
|
||||||
|
* - 0 - **None**
|
||||||
|
* - 10 - **Landscape**
|
||||||
|
* - 20 - **Square**
|
||||||
|
* - 30 - **Portrait**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export enum aspectType {
|
||||||
|
/**
|
||||||
|
* (value: 0)
|
||||||
|
*/
|
||||||
|
None = 0,
|
||||||
|
/**
|
||||||
|
* (value: 10)
|
||||||
|
*/
|
||||||
|
Landscape = 10,
|
||||||
|
/**
|
||||||
|
* (value: 20)
|
||||||
|
*/
|
||||||
|
Square = 20,
|
||||||
|
/**
|
||||||
|
* (value: 30)
|
||||||
|
*/
|
||||||
|
Portrait = 30,
|
||||||
|
}
|
18
src/lib/api/merchant/models/assetCollectionDto.ts
Normal file
18
src/lib/api/merchant/models/assetCollectionDto.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* This Dto represents a reference to a list of stored media assets, such as an image or video.
|
||||||
|
* The assets are referenced by hash digests, which are unique identifiers for the asset.
|
||||||
|
*/
|
||||||
|
export type assetCollectionDto = {
|
||||||
|
/**
|
||||||
|
* This is the kind of media being referenced.
|
||||||
|
*/
|
||||||
|
kind?: string | null;
|
||||||
|
/**
|
||||||
|
* A list of base58-encoded SHA-256 digests for the media assets.
|
||||||
|
*/
|
||||||
|
digests?: Array<string> | null;
|
||||||
|
};
|
30
src/lib/api/merchant/models/authDocumentDto.ts
Normal file
30
src/lib/api/merchant/models/authDocumentDto.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Represents the authentication document that the mobile app creates.
|
||||||
|
* It is signed to prove the identity and includes session and timing details.
|
||||||
|
*/
|
||||||
|
export type authDocumentDto = {
|
||||||
|
/**
|
||||||
|
* Gets or sets the unique session identifier.
|
||||||
|
*/
|
||||||
|
sessionId: string | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets the client IP address fetched from the hub.
|
||||||
|
*/
|
||||||
|
clientIp: string | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets the timestamp when the auth document is issued.
|
||||||
|
*/
|
||||||
|
issuedAt?: string;
|
||||||
|
/**
|
||||||
|
* Gets or sets the timestamp when the auth document expires.
|
||||||
|
*/
|
||||||
|
expiresAt?: string;
|
||||||
|
/**
|
||||||
|
* Gets or sets the mobile app's Ed25519 public key.
|
||||||
|
*/
|
||||||
|
publicKey: string;
|
||||||
|
};
|
25
src/lib/api/merchant/models/authHeaderResponseDto.ts
Normal file
25
src/lib/api/merchant/models/authHeaderResponseDto.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Response DTO for retrieving the authentication header for a session.
|
||||||
|
*/
|
||||||
|
export type authHeaderResponseDto = {
|
||||||
|
/**
|
||||||
|
* Gets or sets the unique session identifier.
|
||||||
|
*/
|
||||||
|
sessionId: string | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets the X-Signed-Auth header value.
|
||||||
|
*/
|
||||||
|
headerValue: string | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets the public key used for authentication.
|
||||||
|
*/
|
||||||
|
publicKey: string | null;
|
||||||
|
/**
|
||||||
|
* The UTC expiry date of the signed authorization header.
|
||||||
|
*/
|
||||||
|
utcExpires: string;
|
||||||
|
};
|
13
src/lib/api/merchant/models/authenticationResultDto.ts
Normal file
13
src/lib/api/merchant/models/authenticationResultDto.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* DTO for the authentication result.
|
||||||
|
*/
|
||||||
|
export type authenticationResultDto = {
|
||||||
|
/**
|
||||||
|
* Gets or sets the session ID that was authenticated.
|
||||||
|
*/
|
||||||
|
sessionId: string | null;
|
||||||
|
};
|
37
src/lib/api/merchant/models/baseCreateSellerRevisionDto.ts
Normal file
37
src/lib/api/merchant/models/baseCreateSellerRevisionDto.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { addressDto } from "./addressDto";
|
||||||
|
import type { createRestaurantRevisionDto } from "./createRestaurantRevisionDto";
|
||||||
|
import type { createSellerRevisionDto } from "./createSellerRevisionDto";
|
||||||
|
import type { gpsLocationDto } from "./gpsLocationDto";
|
||||||
|
import type { imageReferenceDto } from "./imageReferenceDto";
|
||||||
|
import type { scheduleDto } from "./scheduleDto";
|
||||||
|
import type { simpleContactDto } from "./simpleContactDto";
|
||||||
|
/**
|
||||||
|
* Base class for seller data transfer objects, encapsulating common seller properties.
|
||||||
|
* Restaurants, farms, shops and other merchants inherit this class.
|
||||||
|
*/
|
||||||
|
export type baseCreateSellerRevisionDto =
|
||||||
|
| createRestaurantRevisionDto
|
||||||
|
| createSellerRevisionDto
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* Version number of the document.
|
||||||
|
*/
|
||||||
|
documentVersion?: number;
|
||||||
|
schedule?: scheduleDto;
|
||||||
|
address?: addressDto;
|
||||||
|
contactInfo?: simpleContactDto;
|
||||||
|
location?: gpsLocationDto;
|
||||||
|
/**
|
||||||
|
* A key value collection for storing additional information
|
||||||
|
*/
|
||||||
|
additional?: Record<string, any> | null;
|
||||||
|
/**
|
||||||
|
* A list of image references.
|
||||||
|
*/
|
||||||
|
images?: Array<imageReferenceDto> | null;
|
||||||
|
type?: string;
|
||||||
|
};
|
18
src/lib/api/merchant/models/baseOfferDto.ts
Normal file
18
src/lib/api/merchant/models/baseOfferDto.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { bundleDiscountOfferDto } from "./bundleDiscountOfferDto";
|
||||||
|
import type { discountProductOfferDto } from "./discountProductOfferDto";
|
||||||
|
import type { discountTotalOfferDto } from "./discountTotalOfferDto";
|
||||||
|
import type { freeDeliveryOfferDto } from "./freeDeliveryOfferDto";
|
||||||
|
import type { freeItemOfferDto } from "./freeItemOfferDto";
|
||||||
|
export type baseOfferDto =
|
||||||
|
| discountTotalOfferDto
|
||||||
|
| discountProductOfferDto
|
||||||
|
| freeDeliveryOfferDto
|
||||||
|
| freeItemOfferDto
|
||||||
|
| bundleDiscountOfferDto
|
||||||
|
| {
|
||||||
|
readonly type?: string | null;
|
||||||
|
};
|
9
src/lib/api/merchant/models/baseTriggerDto.ts
Normal file
9
src/lib/api/merchant/models/baseTriggerDto.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { triggerEffect } from "./triggerEffect";
|
||||||
|
export type baseTriggerDto = {
|
||||||
|
readonly type?: string | null;
|
||||||
|
effect?: triggerEffect;
|
||||||
|
};
|
44
src/lib/api/merchant/models/basketDto.ts
Normal file
44
src/lib/api/merchant/models/basketDto.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { dispatchMethodType } from "./dispatchMethodType";
|
||||||
|
import type { gpsLocationDto } from "./gpsLocationDto";
|
||||||
|
import type { selectedProductDto } from "./selectedProductDto";
|
||||||
|
/**
|
||||||
|
* Represents a customer's basket containing selected products for checkout processing.
|
||||||
|
*/
|
||||||
|
export type basketDto = {
|
||||||
|
/**
|
||||||
|
* Gets the unique identifier for this basket.
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
/**
|
||||||
|
* Gets the unique identifier of the seller.
|
||||||
|
*/
|
||||||
|
sellerId: string;
|
||||||
|
/**
|
||||||
|
* When the order is scheduled for delivery or pickup, this property indicates the scheduled time.
|
||||||
|
*/
|
||||||
|
scheduledFor?: string;
|
||||||
|
/**
|
||||||
|
* Gets the collection of selected products in this basket.
|
||||||
|
*/
|
||||||
|
items?: Array<selectedProductDto> | null;
|
||||||
|
gpsLocation: gpsLocationDto;
|
||||||
|
/**
|
||||||
|
* Gets or sets whether community change is enabled for this basket.
|
||||||
|
*/
|
||||||
|
isCommunityChangeEnabled?: boolean;
|
||||||
|
dispatchType?: dispatchMethodType;
|
||||||
|
/**
|
||||||
|
* Optional: The customer has selected a delivery price that was on offer.
|
||||||
|
*/
|
||||||
|
selectedDeliveryPrice?: {
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
} | null;
|
||||||
|
};
|
11
src/lib/api/merchant/models/bundleDiscountOfferDto.ts
Normal file
11
src/lib/api/merchant/models/bundleDiscountOfferDto.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { discountType } from "./discountType";
|
||||||
|
export type bundleDiscountOfferDto = {
|
||||||
|
readonly type?: "bundle_discount" | null;
|
||||||
|
productIds: Array<string> | null;
|
||||||
|
discountType: discountType;
|
||||||
|
value: number;
|
||||||
|
};
|
10
src/lib/api/merchant/models/bundleTriggerDto.ts
Normal file
10
src/lib/api/merchant/models/bundleTriggerDto.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { triggerEffect } from "./triggerEffect";
|
||||||
|
export type bundleTriggerDto = {
|
||||||
|
effect?: triggerEffect;
|
||||||
|
productIds?: Array<string> | null;
|
||||||
|
readonly type?: "bundle" | null;
|
||||||
|
};
|
122
src/lib/api/merchant/models/calculatedCheckoutDto.ts
Normal file
122
src/lib/api/merchant/models/calculatedCheckoutDto.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { calculatedProductDto } from "./calculatedProductDto";
|
||||||
|
import type { productUnavailableDto } from "./productUnavailableDto";
|
||||||
|
/**
|
||||||
|
* Represents a calculated checkout with comprehensive pricing information for all items, including taxes, discounts, and delivery fees.
|
||||||
|
*/
|
||||||
|
export type calculatedCheckoutDto = {
|
||||||
|
/**
|
||||||
|
* Must be set if the basket is to be considered valid.
|
||||||
|
*/
|
||||||
|
isValid?: boolean;
|
||||||
|
/**
|
||||||
|
* Gets or sets the collection of calculated products in this checkout.
|
||||||
|
*/
|
||||||
|
items?: Array<calculatedProductDto> | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets the subtotal of all items before applying discounts.
|
||||||
|
*/
|
||||||
|
itemSubTotal?: {
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets the subtotal of all items with any discounts applied.
|
||||||
|
*/
|
||||||
|
itemDiscountedSubTotal?: {
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets the tax amount applied to this checkout, if any.
|
||||||
|
*/
|
||||||
|
tax?: {
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets the community contribution amount, if any.
|
||||||
|
*/
|
||||||
|
communityChange?: {
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* The user selected to enable community change.
|
||||||
|
*/
|
||||||
|
communityChangeIncluded?: boolean;
|
||||||
|
/**
|
||||||
|
* Returns the value of any discount applied at the order level.
|
||||||
|
*/
|
||||||
|
discount?: {
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* Returns the delivery fee, if any.
|
||||||
|
*/
|
||||||
|
delivery?: {
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets additional charges or fees associated with this checkout.
|
||||||
|
*/
|
||||||
|
extras?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
}
|
||||||
|
> | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets the final total price of this checkout after all calculations.
|
||||||
|
*/
|
||||||
|
final?: {
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* Gets the collection of all active promotion IDs applied to this checkout.
|
||||||
|
*/
|
||||||
|
readonly activePromotionIds?: Array<string> | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets the list of unavailable products related to this checkout.
|
||||||
|
*/
|
||||||
|
unavailableProducts?: Array<productUnavailableDto> | null;
|
||||||
|
/**
|
||||||
|
* When doing cash payments, this is the first hint for the amount of cash the customer has prepared for the order.
|
||||||
|
*/
|
||||||
|
cashChangeHint1?: number | null;
|
||||||
|
/**
|
||||||
|
* When doing cash payments, this is the second hint for the amount of cash the customer has prepared for the order.
|
||||||
|
*/
|
||||||
|
cashChangeHint2?: number | null;
|
||||||
|
};
|
64
src/lib/api/merchant/models/calculatedProductDto.ts
Normal file
64
src/lib/api/merchant/models/calculatedProductDto.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Represents a calculated product with pricing information, including subtotal, discounts, and final price.
|
||||||
|
*/
|
||||||
|
export type calculatedProductDto = {
|
||||||
|
/**
|
||||||
|
* Represents the unique 'line' identifier of the item in the checkout.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Gets the unique identifier of the product.
|
||||||
|
*/
|
||||||
|
productId: string;
|
||||||
|
/**
|
||||||
|
* This value will be null if the Final amount is not a discount.
|
||||||
|
*/
|
||||||
|
subTotal: {
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* This value will be null if the Final amount is not a discount.
|
||||||
|
*/
|
||||||
|
discountedTotal?: {
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets additional charges or fees associated with this product.
|
||||||
|
*/
|
||||||
|
extras?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
}
|
||||||
|
> | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets the final price of this product after all calculations.
|
||||||
|
*/
|
||||||
|
final?: {
|
||||||
|
/**
|
||||||
|
* Currency code ISO 4217
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
value?: number;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* Gets or sets the collection of active promotion IDs applied to this product.
|
||||||
|
*/
|
||||||
|
activePromotionIds?: Array<string> | null;
|
||||||
|
};
|
22
src/lib/api/merchant/models/categoryCompositeDto.ts
Normal file
22
src/lib/api/merchant/models/categoryCompositeDto.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { imageReferenceDto } from "./imageReferenceDto";
|
||||||
|
import type { localizedTextDto } from "./localizedTextDto";
|
||||||
|
import type { scheduleDto } from "./scheduleDto";
|
||||||
|
import type { tag } from "./tag";
|
||||||
|
export type categoryCompositeDto = {
|
||||||
|
tags?: Array<tag> | null;
|
||||||
|
additional?: Record<string, any> | null;
|
||||||
|
images?: Array<imageReferenceDto> | null;
|
||||||
|
schedule?: scheduleDto;
|
||||||
|
sortOrder?: number;
|
||||||
|
id?: string;
|
||||||
|
revisionId?: string;
|
||||||
|
promotionIds?: Array<string> | null;
|
||||||
|
title?: localizedTextDto;
|
||||||
|
description?: localizedTextDto;
|
||||||
|
isAvailable?: boolean;
|
||||||
|
isStopListed?: boolean;
|
||||||
|
};
|
13
src/lib/api/merchant/models/categoryDto.ts
Normal file
13
src/lib/api/merchant/models/categoryDto.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { iTriggerDto } from "./iTriggerDto";
|
||||||
|
export type categoryDto = {
|
||||||
|
promotionIds?: Array<string> | null;
|
||||||
|
sellerId?: string;
|
||||||
|
availabilityTriggers?: Array<iTriggerDto> | null;
|
||||||
|
id?: string;
|
||||||
|
updatedByUserId?: string;
|
||||||
|
utcUpdated?: string;
|
||||||
|
};
|
10
src/lib/api/merchant/models/categoryQueryRequestDto.ts
Normal file
10
src/lib/api/merchant/models/categoryQueryRequestDto.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type categoryQueryRequestDto = {
|
||||||
|
categoryIds?: Array<string> | null;
|
||||||
|
sellerId?: string | null;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
};
|
24
src/lib/api/merchant/models/categoryRevisionDto.ts
Normal file
24
src/lib/api/merchant/models/categoryRevisionDto.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { imageReferenceDto } from "./imageReferenceDto";
|
||||||
|
import type { localizedTextDto } from "./localizedTextDto";
|
||||||
|
import type { scheduleDto } from "./scheduleDto";
|
||||||
|
import type { tag } from "./tag";
|
||||||
|
export type categoryRevisionDto = {
|
||||||
|
tags?: Array<tag> | null;
|
||||||
|
additional?: Record<string, any> | null;
|
||||||
|
images?: Array<imageReferenceDto> | null;
|
||||||
|
schedule?: scheduleDto;
|
||||||
|
sortOrder?: number;
|
||||||
|
revisionId?: string;
|
||||||
|
categoryId?: string;
|
||||||
|
creatorId?: string;
|
||||||
|
utcCreated?: string;
|
||||||
|
title?: localizedTextDto;
|
||||||
|
description?: localizedTextDto;
|
||||||
|
isPublished?: boolean;
|
||||||
|
utcPublished?: string | null;
|
||||||
|
publishedByUserId?: string | null;
|
||||||
|
};
|
@ -0,0 +1,14 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type categoryRevisionQueryRequestDto = {
|
||||||
|
categoryId?: string | null;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
utcCreatedFrom?: string | null;
|
||||||
|
utcCreatedTo?: string | null;
|
||||||
|
isPublished?: boolean | null;
|
||||||
|
publishedByUserId?: string | null;
|
||||||
|
utcCreated?: string | null;
|
||||||
|
};
|
7
src/lib/api/merchant/models/categoryStopListDto.ts
Normal file
7
src/lib/api/merchant/models/categoryStopListDto.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type categoryStopListDto = {
|
||||||
|
categoryIds: Array<string>;
|
||||||
|
};
|
19
src/lib/api/merchant/models/categoryViewDto.ts
Normal file
19
src/lib/api/merchant/models/categoryViewDto.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { imageReferenceDto } from "./imageReferenceDto";
|
||||||
|
import type { localizedTextDto } from "./localizedTextDto";
|
||||||
|
import type { scheduleDto } from "./scheduleDto";
|
||||||
|
import type { tag } from "./tag";
|
||||||
|
export type categoryViewDto = {
|
||||||
|
tags?: Array<tag> | null;
|
||||||
|
additional?: Record<string, any> | null;
|
||||||
|
images?: Array<imageReferenceDto> | null;
|
||||||
|
schedule?: scheduleDto;
|
||||||
|
sortOrder?: number;
|
||||||
|
revisionId?: string;
|
||||||
|
categoryId?: string;
|
||||||
|
title?: localizedTextDto;
|
||||||
|
description?: localizedTextDto;
|
||||||
|
};
|
27
src/lib/api/merchant/models/checkoutOptionsDto.ts
Normal file
27
src/lib/api/merchant/models/checkoutOptionsDto.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { deliveryLocationDto } from "./deliveryLocationDto";
|
||||||
|
import type { dispatchMethodType } from "./dispatchMethodType";
|
||||||
|
import type { simpleContactDto } from "./simpleContactDto";
|
||||||
|
/**
|
||||||
|
* Data transfer object for restaurant checkout options
|
||||||
|
*/
|
||||||
|
export type checkoutOptionsDto = {
|
||||||
|
dispatchType?: dispatchMethodType;
|
||||||
|
/**
|
||||||
|
* Gets or sets the scheduled date and time for the order.
|
||||||
|
*/
|
||||||
|
scheduledFor?: string | null;
|
||||||
|
contact?: simpleContactDto;
|
||||||
|
/**
|
||||||
|
* Gets or sets the cash amount prepared for the order.
|
||||||
|
*/
|
||||||
|
cashAmount?: number | null;
|
||||||
|
deliveryLocation?: deliveryLocationDto;
|
||||||
|
/**
|
||||||
|
* Gets or sets whether community change is enabled for this basket.
|
||||||
|
*/
|
||||||
|
isCommunityChangeEnabled?: boolean;
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user