forked from Qortal/q-shop
chore(release): v1.1.0\n\n- Bump version to 1.1.0\n- Add release notes and user announcement\n- Add Gitea release workflow\n- Finalize coin support and UI fixes
This commit is contained in:
36
.gitea/workflows/release.yml
Normal file
36
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
name: build-and-zip
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node LTS
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Zip dist
|
||||
run: |
|
||||
cd dist
|
||||
zip -r ../dist.zip .
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist.zip
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -22,3 +22,7 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# More
|
||||
.runner
|
||||
.gitea.env
|
||||
|
||||
4
config.yaml
Normal file
4
config.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
runner:
|
||||
capacity: 1
|
||||
labels:
|
||||
- 'self-hosted'
|
||||
50
docs/RELEASE_NOTES_v1.1.0.md
Normal file
50
docs/RELEASE_NOTES_v1.1.0.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Q‑Shop v1.1.0 — Release Notes
|
||||
|
||||
Release date: 2025‑08‑23
|
||||
|
||||
## Highlights
|
||||
- New coin support: BTC, LTC, DOGE, DGB, RVN in addition to QORT (native) and ARRR.
|
||||
- Improved store editing experience and safety controls.
|
||||
- Clearer pricing UX with dynamic exchange card and date sorting restored.
|
||||
|
||||
## What’s New
|
||||
- Supported coins
|
||||
- Added BTC, LTC, DOGE, DGB, RVN across the app: store setup, badges, filtering, pricing, and icons.
|
||||
- Centralized PNG icon mapping for all coins in `src/constants/coin-icons.ts`.
|
||||
- Edit Store modal
|
||||
- Cancel reliably closes the modal.
|
||||
- Advanced Settings restored with a “Recreate Shop Data” action:
|
||||
- Publishes a fresh empty Data Container (DOCUMENT) to QDN for the current shop.
|
||||
- Resets cached product lists to avoid UI inconsistencies.
|
||||
- Confirmation modal with warnings to prevent accidental use.
|
||||
- Supported Coins dropdown now uses PNG icons for all coins.
|
||||
- Local draft preservation: unsaved edits persist while the modal is open (localStorage) and can be cleared.
|
||||
- Sidebar filters (Store page)
|
||||
- Prices In: de‑duplicated header; single clear section.
|
||||
- Exchange Rate card:
|
||||
- Hidden when viewing prices in QORT.
|
||||
- Shows dynamic coin icon for the selected coin.
|
||||
- Displays both directions: `1 QORT = X COIN` and `1 COIN = Y QORT`.
|
||||
- Date Product Added: restored “Most Recent” and “Oldest” sort options.
|
||||
|
||||
## Improvements
|
||||
- Pricing lookup now integrates per‑coin keys via `getPriceHint()` with graceful fallback to QORT when rate is unavailable.
|
||||
- Safer store edit flow with explicit error notifications and loading indicators.
|
||||
|
||||
## Fixes
|
||||
- TypeScript/JSX issues in `EditStoreModal.tsx` causing build failures.
|
||||
- Inconsistent close behavior for the edit modal.
|
||||
- Mixed icon sources (SVG vs. PNG) resolved for consistency.
|
||||
|
||||
## Developer Notes
|
||||
- Build: `npm ci && npm run build` (Node LTS). Output in `dist/`.
|
||||
- Icons: coin assets located under `src/assets/img/` and referenced via `coinPng()`.
|
||||
- Recreate Shop Data publishes `identifier: <storeId>-datacontainer` to QDN with an empty `products` map. Use only for recovery.
|
||||
|
||||
## Upgrade Guide
|
||||
- No config changes required.
|
||||
- After updating, shop owners can edit their store to add newly supported coins and addresses. Pricing UX updates apply automatically.
|
||||
|
||||
## Known Considerations
|
||||
- Exchange rates rely on recent trade portal data; when unavailable, the app reverts to QORT pricing and notifies the user.
|
||||
|
||||
91
docs/ROAD_TO_RELEASE_v1.1.0.md
Normal file
91
docs/ROAD_TO_RELEASE_v1.1.0.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Q‑Shop v1.1.0 — Road to Release
|
||||
|
||||
Goal: Ship new coin support and UI fixes with a clean build and verified UX.
|
||||
|
||||
## Scope
|
||||
- Stabilize Edit Store modal interactions and advanced actions.
|
||||
- Unify coin iconography across dropdowns and cards using PNG assets.
|
||||
- Clean up and enhance sidebar filtering UX (Prices In, Exchange Rate, Date sort).
|
||||
|
||||
## Tasks
|
||||
1) EditStoreModal: Cancel button closes modal
|
||||
- Wire Cancel to dispatch `toggleEditStoreModal(false)` via GlobalWrapper pattern.
|
||||
- Maintain existing optional `closeEditStoreModal` prop flow for back‑compat.
|
||||
|
||||
2) EditStoreModal: Coin dropdown icons use PNGs
|
||||
- Replace QORT/ARRR SVG usage with PNGs via `coinPng()`.
|
||||
- Ensure all supported coins (QORT, ARRR, BTC, LTC, DOGE, DGB, RVN) have PNGs imported in `src/constants/coin-icons.ts`.
|
||||
|
||||
3) EditStoreModal: Restore Advanced Settings + Recreate Shop Data
|
||||
- Add an “Advanced Settings” checkbox next to action buttons.
|
||||
- When checked, show a red “Recreate Shop Data” button.
|
||||
- Handler publishes an empty Data Container document to QDN using:
|
||||
- identifier: `${storeIdentifier}-${DATA_CONTAINER_BASE}`
|
||||
- structure: `{ storeId, shortStoreId, owner, products: {} }`
|
||||
- Show progress/errors via global loader + notifications. Confirm outcome.
|
||||
|
||||
4) Sidebar: Fix Prices In duplicate header
|
||||
- In `src/pages/Store/Store/Store.tsx`, remove the extra Typography header under the FiltersTitle.
|
||||
|
||||
5) Sidebar: Exchange rate card behavior
|
||||
- Hide the exchange rate card when `coinToUse === 'QORT'`.
|
||||
- Use `coinPng('QORT')` for the left icon and `coinPng(coinToUse)` for the right icon.
|
||||
- Keep title “1 QORT = {rate} {COIN}” and error fallback notifications as‑is.
|
||||
|
||||
6) Sidebar: Restore Date Product Added sort checkboxes
|
||||
- Add a new “Date Product Added” group with:
|
||||
- “Most Recent” → sets `filterDate = DateFilter.newest` and resets price filter
|
||||
- “Oldest” → sets `filterDate = DateFilter.oldest` and resets price filter
|
||||
- Leverage existing `filterDate` logic already present in the selector.
|
||||
|
||||
## File Targets
|
||||
- `src/components/modals/EditStoreModal.tsx`
|
||||
- Cancel wiring to close
|
||||
- PNG icons in coin dropdown
|
||||
- Advanced Settings checkbox + Recreate Shop Data button and handler
|
||||
- `src/pages/Store/Store/Store.tsx`
|
||||
- Remove duplicate Prices In header
|
||||
- Conditional exchange rate card with dynamic right icon
|
||||
- Add “Date Product Added” group and two checkboxes
|
||||
- `src/constants/coin-icons.ts`
|
||||
- Verify PNG imports for BTC/LTC/DOGE/DGB/RVN exist and are referenced by `coinPng()`
|
||||
- `src/assets/img/*`
|
||||
- Ensure PNG assets for all supported coins are present; add missing ones if needed.
|
||||
|
||||
## Validation
|
||||
- Typecheck + build: `npm run build` on Node LTS.
|
||||
- Manual checks:
|
||||
- Edit Store modal: Cancel closes; Advanced → Recreate button visible and functional.
|
||||
- Coin dropdown in Edit/Store pages shows PNG icons for all coins.
|
||||
- Prices In has a single header; exchange card hidden for QORT and dynamic for others.
|
||||
- Exchange card shows both directions: `1 QORT = X COIN` and `1 COIN = Y QORT`.
|
||||
- Date sort toggles between Most Recent / Oldest and matches product ordering.
|
||||
|
||||
## Release Prep
|
||||
- Smoke test store creation/edit flows and product list in a test profile.
|
||||
- Verify new coin prices and iconography across product cards and details.
|
||||
- Update changelog/README for new coins and UI fixes.
|
||||
- Bump `package.json` to 1.1.0.
|
||||
- Add `docs/RELEASE_NOTES_v1.1.0.md` and `docs/USER_ANNOUNCEMENT_v1.1.0.md`.
|
||||
- Add `.gitea/workflows/release.yml` to build on tag and zip `dist/` as artifact.
|
||||
- Create tag `v1.1.0` and push to trigger workflow.
|
||||
- Draft Gitea release and attach `dist.zip` (then publish to Qortal).
|
||||
|
||||
## Open Questions
|
||||
- Recreate Shop Data: ok to publish immediately from Edit modal, or should we gate behind an extra confirmation modal?
|
||||
- Should `onPublishParamEdit` include `storeIdentifier` for edits, or remain excluded as now?
|
||||
|
||||
## Progress Log
|
||||
- Implemented EditStoreModal fixes:
|
||||
- Cancel now dispatches `toggleEditStoreModal(false)` and supports legacy close flag.
|
||||
- Restored Advanced Settings with “Recreate Shop Data” flow and confirmation modal; publishes empty data container and resets product cache.
|
||||
- Switched Supported Coins dropdown icons to PNG via `coinPng()` for all coins.
|
||||
- Sidebar Store page updates:
|
||||
- Removed duplicate “Prices In” header.
|
||||
- Exchange rate card now hidden for QORT and shows dynamic coin icon for the selected coin.
|
||||
- Restored “Date Product Added” section with Most Recent/Oldest.
|
||||
- Verified a clean production build with Vite.
|
||||
- Added reverse rate display to exchange card.
|
||||
- Bumped version to 1.1.0 in `package.json`.
|
||||
- Added release notes and user announcement under `docs/`.
|
||||
- Added Gitea workflow to build and archive `dist.zip` on tag.
|
||||
11
docs/USER_ANNOUNCEMENT_v1.1.0.md
Normal file
11
docs/USER_ANNOUNCEMENT_v1.1.0.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Q‑Shop v1.1.0 — What’s New
|
||||
|
||||
Q‑Shop just got a big upgrade. Here’s the short version:
|
||||
|
||||
- More coins: In addition to QORT and ARRR, your shops can now price and accept BTC, LTC, DOGE, DGB, and RVN.
|
||||
- Easier editing: The Edit Shop modal is smoother — Cancel closes properly, and you can quickly clear unsaved changes.
|
||||
- Advanced tools: Need to reset your shop’s data container? There’s a clear “Recreate Shop Data” option under Advanced Settings (with a confirmation to keep you safe).
|
||||
- Clearer prices: Pick your “Prices In” coin from the sidebar and see live exchange info — including both 1 QORT = X COIN and 1 COIN = Y QORT — with the right coin icons.
|
||||
- Better sorting: “Date Product Added” is back, so you can view the newest or oldest items first.
|
||||
|
||||
That’s it! Update to 1.1.0, add your wallet address(es) for the new coins, and enjoy a smoother Q‑Shop experience.
|
||||
41
package-lock.json
generated
41
package-lock.json
generated
@@ -1,18 +1,20 @@
|
||||
{
|
||||
"name": "q-blog",
|
||||
"version": "0.0.0",
|
||||
"name": "q-shop",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "q-blog",
|
||||
"version": "0.0.0",
|
||||
"name": "q-shop",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.13",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@scure/base": "^1.1.5",
|
||||
"@types/react-grid-layout": "^1.3.2",
|
||||
"axios": "^1.3.4",
|
||||
"compressorjs": "^1.2.1",
|
||||
@@ -1111,6 +1113,18 @@
|
||||
"react": "^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
|
||||
@@ -1444,6 +1458,15 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
|
||||
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.3.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.41.tgz",
|
||||
@@ -4745,6 +4768,11 @@
|
||||
"react-is": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="
|
||||
},
|
||||
"@popperjs/core": {
|
||||
"version": "2.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
|
||||
@@ -4996,6 +5024,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.4.0.tgz",
|
||||
"integrity": "sha512-BJ9SxXux8zAg991UmT8slpwpsd31K1dHHbD3Ba4VzD+liLQ4WAMSxQp2d2ZPRPfN0jN2NPRowcSSoM7lCaF08Q=="
|
||||
},
|
||||
"@scure/base": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
|
||||
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="
|
||||
},
|
||||
"@swc/core": {
|
||||
"version": "1.3.41",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.41.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "q-shop",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -39,7 +39,9 @@
|
||||
"slate": "^0.91.4",
|
||||
"slate-history": "^0.86.0",
|
||||
"slate-react": "^0.91.11",
|
||||
"ua-parser-js": "^1.0.37"
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"@scure/base": "^1.1.5",
|
||||
"@noble/hashes": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mui/types": "^7.2.3",
|
||||
|
||||
62
src/components/common/PaymentProof.tsx
Normal file
62
src/components/common/PaymentProof.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import LaunchIcon from "@mui/icons-material/Launch";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import { Tooltip, Chip } from "@mui/material";
|
||||
import { explorerTxUrl } from "../../lib/explorers";
|
||||
import { CoinTicker } from "../../constants/coin-registry";
|
||||
|
||||
type Props = {
|
||||
coin: string | CoinTicker;
|
||||
proof?: string | null;
|
||||
label?: string;
|
||||
size?: "small" | "medium";
|
||||
};
|
||||
|
||||
const truncate = (s: string, head = 8, tail = 6) => {
|
||||
if (!s) return "";
|
||||
if (s.length <= head + tail + 3) return s;
|
||||
return s.slice(0, head) + "..." + s.slice(-tail);
|
||||
};
|
||||
|
||||
const PaymentProof: React.FC<Props> = ({ coin, proof, label, size = "small" }) => {
|
||||
const ticker = String(coin || "").toUpperCase() as CoinTicker;
|
||||
const id = (proof || "").trim();
|
||||
if (!id) return null;
|
||||
|
||||
const href = explorerTxUrl(ticker, id);
|
||||
|
||||
const onCopy = () => {
|
||||
try { navigator.clipboard.writeText(id); } catch {}
|
||||
};
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Tooltip title={`View on explorer: ${ticker}`}>
|
||||
<Chip
|
||||
size={size}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
label={label || `Proof`}
|
||||
onClick={() => window.open(href, "_blank", "noopener,noreferrer")}
|
||||
onDelete={onCopy}
|
||||
deleteIcon={<ContentCopyIcon fontSize="small" />}
|
||||
icon={<LaunchIcon fontSize="small" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={id}>
|
||||
<Chip
|
||||
size={size}
|
||||
variant="outlined"
|
||||
label={label || truncate(id)}
|
||||
onDelete={onCopy}
|
||||
deleteIcon={<ContentCopyIcon fontSize="small" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentProof;
|
||||
@@ -37,6 +37,7 @@ import { supportedCoinsArray } from "../../constants/supported-coins";
|
||||
import { QortalSVG } from "../../assets/svgs/QortalSVG";
|
||||
import { ARRRSVG } from "../../assets/svgs/ARRRSVG";
|
||||
import { setNotification } from "../../state/features/notificationsSlice";
|
||||
|
||||
export interface ForeignCoins {
|
||||
[key: string]: string;
|
||||
}
|
||||
@@ -60,7 +61,6 @@ interface CreateStoreModalProps {
|
||||
username: string;
|
||||
}
|
||||
|
||||
|
||||
const CreateStoreModal: React.FC<CreateStoreModalProps> = ({
|
||||
open,
|
||||
closeCreateStoreModal,
|
||||
@@ -78,11 +78,9 @@ const CreateStoreModal: React.FC<CreateStoreModalProps> = ({
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [storeIdentifier, setStoreIdentifier] = useState("");
|
||||
const [logo, setLogo] = useState<string | null>(null);
|
||||
const [supportedCoinsSelected, setSupportedCoinsSelected] = useState<
|
||||
string[]
|
||||
>(["QORT"]);
|
||||
const [supportedCoinsSelected, setSupportedCoinsSelected] = useState<string[]>(["QORT"]);
|
||||
const [qortWalletAddress, setQortWalletAddress] = useState<string>("");
|
||||
const [arrrWalletAddress, setArrrWalletAddress] = useState<string>("");
|
||||
const [foreignWallets, setForeignWallets] = useState<Record<string, string>>({ ARRR: "" });
|
||||
|
||||
const handlePublish = async (): Promise<void> => {
|
||||
try {
|
||||
@@ -91,12 +89,14 @@ const CreateStoreModal: React.FC<CreateStoreModalProps> = ({
|
||||
setErrorMessage("A logo is required");
|
||||
return;
|
||||
}
|
||||
const foreignCoins: ForeignCoins = {
|
||||
ARRR: arrrWalletAddress
|
||||
}
|
||||
supportedCoinsSelected.filter((coin)=> coin !== 'QORT').forEach((item: string)=> {
|
||||
if(!foreignCoins[item]) throw new Error(`Please add a ${item} address`)
|
||||
})
|
||||
const foreignCoins: ForeignCoins = {};
|
||||
supportedCoinsSelected
|
||||
.filter((coin) => coin !== "QORT")
|
||||
.forEach((item: string) => {
|
||||
const addr = foreignWallets[item]?.trim();
|
||||
if (!addr) throw new Error(`Please add a ${item} address`);
|
||||
foreignCoins[item] = addr;
|
||||
});
|
||||
await onPublish({
|
||||
title,
|
||||
description,
|
||||
@@ -104,10 +104,8 @@ const CreateStoreModal: React.FC<CreateStoreModalProps> = ({
|
||||
location,
|
||||
storeIdentifier,
|
||||
logo,
|
||||
foreignCoins: {
|
||||
ARRR: arrrWalletAddress
|
||||
},
|
||||
supportedCoins: supportedCoinsSelected
|
||||
foreignCoins: foreignCoins,
|
||||
supportedCoins: supportedCoinsSelected,
|
||||
});
|
||||
} catch (error: any) {
|
||||
setErrorMessage(error.message);
|
||||
@@ -118,8 +116,9 @@ const CreateStoreModal: React.FC<CreateStoreModalProps> = ({
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setErrorMessage("");
|
||||
setArrrWalletAddress("")
|
||||
setSupportedCoinsSelected(["QORT"])
|
||||
// Reset foreign wallets (ARRR preserved as known default key)
|
||||
setForeignWallets({ ARRR: "" });
|
||||
setSupportedCoinsSelected(["QORT"]);
|
||||
dispatch(toggleCreateStoreModal(false));
|
||||
};
|
||||
|
||||
@@ -157,39 +156,35 @@ const CreateStoreModal: React.FC<CreateStoreModalProps> = ({
|
||||
|
||||
const handleChipRemove = (chip: string) => {
|
||||
if (chip === "QORT") return;
|
||||
setSupportedCoinsSelected(prevChips => prevChips.filter(c => c !== chip));
|
||||
setSupportedCoinsSelected((prevChips) => prevChips.filter((c) => c !== chip));
|
||||
};
|
||||
|
||||
const importAddress = async (coin: string)=> {
|
||||
const importAddress = async (coin: string) => {
|
||||
try {
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
|
||||
|
||||
const res = await qortalRequest({
|
||||
action: 'GET_USER_WALLET',
|
||||
coin
|
||||
})
|
||||
|
||||
if(res?.address){
|
||||
setArrrWalletAddress(res.address)
|
||||
action: "GET_USER_WALLET",
|
||||
coin,
|
||||
});
|
||||
|
||||
if (res?.address) {
|
||||
setForeignWallets((prev) => ({ ...prev, [coin]: res.address }));
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
setNotification({
|
||||
alertType: "error",
|
||||
msg: "Unable to import ARRR address. Please insert it manually",
|
||||
msg: `Unable to import ${coin} address. Please insert it manually`,
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
aria-labelledby="modal-title"
|
||||
aria-describedby="modal-description"
|
||||
>
|
||||
<Modal open={open} aria-labelledby="modal-title" aria-describedby="modal-description">
|
||||
<ModalBody>
|
||||
<ModalTitle id="modal-title">Create Shop</ModalTitle>
|
||||
{!logo ? (
|
||||
@@ -280,8 +275,9 @@ const CreateStoreModal: React.FC<CreateStoreModalProps> = ({
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
{/* QORT Wallet Input Field */}
|
||||
{/* <WalletRow>
|
||||
{/* QORT Wallet Input Field (currently unused) */}
|
||||
{/*
|
||||
<WalletRow>
|
||||
<CustomInputField
|
||||
id="modal-qort-wallet-input"
|
||||
label="QORT Wallet Address"
|
||||
@@ -300,43 +296,39 @@ const CreateStoreModal: React.FC<CreateStoreModalProps> = ({
|
||||
title="Import your QORT Wallet Address from your current account"
|
||||
>
|
||||
<IconButton disableFocusRipple={true} disableRipple={true}>
|
||||
<DownloadArrrWalletIcon
|
||||
color={theme.palette.text.primary}
|
||||
height="40"
|
||||
width="40"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</WalletRow> */}
|
||||
|
||||
{/* ARRR Wallet Input Field */}
|
||||
<WalletRow>
|
||||
<CustomInputField
|
||||
id="modal-arrr-wallet-input"
|
||||
label="ARRR Wallet Address"
|
||||
value={arrrWalletAddress}
|
||||
onChange={(e: any) => {
|
||||
setArrrWalletAddress(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
<Tooltip
|
||||
TransitionComponent={Zoom}
|
||||
placement="top"
|
||||
arrow={true}
|
||||
title="Import your ARRR Wallet Address from your current account"
|
||||
>
|
||||
<IconButton disableFocusRipple={true} disableRipple={true} onClick={()=> importAddress('ARRR')}>
|
||||
<DownloadArrrWalletIcon
|
||||
color={theme.palette.text.primary}
|
||||
height="40"
|
||||
width="40"
|
||||
/>
|
||||
<DownloadArrrWalletIcon color={theme.palette.text.primary} height="40" width="40" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</WalletRow>
|
||||
*/}
|
||||
|
||||
{/* Foreign coin wallet inputs */}
|
||||
{supportedCoinsSelected
|
||||
.filter((coin) => coin !== "QORT")
|
||||
.map((coin) => (
|
||||
<WalletRow key={coin}>
|
||||
<CustomInputField
|
||||
id={`modal-wallet-${coin}`}
|
||||
label={`${coin} Wallet Address`}
|
||||
value={foreignWallets[coin] || ""}
|
||||
onChange={(e: any) => setForeignWallets((prev) => ({ ...prev, [coin]: e.target.value }))}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
<Tooltip
|
||||
TransitionComponent={Zoom}
|
||||
placement="top"
|
||||
arrow={true}
|
||||
title={`Import your ${coin} Wallet Address from your current account`}
|
||||
>
|
||||
<IconButton disableFocusRipple={true} disableRipple={true} onClick={() => importAddress(coin)}>
|
||||
<DownloadArrrWalletIcon color={theme.palette.text.primary} height="40" width="40" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</WalletRow>
|
||||
))}
|
||||
|
||||
{/* Coin selection available for your shop */}
|
||||
<FilterSelect
|
||||
disableClearable
|
||||
@@ -355,9 +347,7 @@ const CreateStoreModal: React.FC<CreateStoreModalProps> = ({
|
||||
<FiltersChip
|
||||
key={value}
|
||||
label={value}
|
||||
onDelete={
|
||||
value !== "QORT" ? () => handleChipRemove(value) : undefined
|
||||
}
|
||||
onDelete={value !== "QORT" ? () => handleChipRemove(value) : undefined}
|
||||
clickable={value === "QORT" ? false : true}
|
||||
/>
|
||||
);
|
||||
@@ -367,28 +357,17 @@ const CreateStoreModal: React.FC<CreateStoreModalProps> = ({
|
||||
const isDisabled = option === "QORT";
|
||||
return (
|
||||
<FiltersOption {...props}>
|
||||
<FiltersCheckbox
|
||||
disabled={isDisabled}
|
||||
checked={supportedCoinsSelected.some(coin => coin === option)}
|
||||
/>
|
||||
<FiltersCheckbox disabled={isDisabled} checked={supportedCoinsSelected.some((coin) => coin === option)} />
|
||||
{option === "QORT" ? (
|
||||
<QortalSVG
|
||||
height="22"
|
||||
width="22"
|
||||
color={theme.palette.text.primary}
|
||||
/>
|
||||
<QortalSVG height="22" width="22" color={theme.palette.text.primary} />
|
||||
) : option === "ARRR" ? (
|
||||
<ARRRSVG
|
||||
height="22"
|
||||
width="22"
|
||||
color={theme.palette.text.primary}
|
||||
/>
|
||||
<ARRRSVG height="22" width="22" color={theme.palette.text.primary} />
|
||||
) : null}
|
||||
<span style={{ marginLeft: "5px" }}>{option}</span>
|
||||
</FiltersOption>
|
||||
);
|
||||
}}
|
||||
renderInput={params => (
|
||||
renderInput={(params) => (
|
||||
<FilterSelectMenuItems
|
||||
{...params}
|
||||
label="Supported Coins"
|
||||
|
||||
@@ -1,42 +1,33 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ChangeEvent, useState, useEffect } from "react";
|
||||
import {
|
||||
Typography,
|
||||
Modal,
|
||||
FormControl,
|
||||
useTheme,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Zoom,
|
||||
Box,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
toggleCreateStoreModal,
|
||||
setDataContainer,
|
||||
resetListProducts,
|
||||
resetProducts
|
||||
} from "../../state/features/globalSlice";
|
||||
import { RootState } from "../../state/store";
|
||||
import ImageUploader from "../common/ImageUploader";
|
||||
import {
|
||||
ModalTitle,
|
||||
StoreLogoPreview,
|
||||
AddLogoButton,
|
||||
AddLogoIcon,
|
||||
WalletRow,
|
||||
TimesIcon,
|
||||
LogoPreviewRow,
|
||||
CustomInputField,
|
||||
ModalBody,
|
||||
ButtonRow,
|
||||
CancelButton,
|
||||
CreateButton,
|
||||
CustomInputField,
|
||||
WalletRow,
|
||||
DownloadArrrWalletIcon,
|
||||
LogoPreviewRow,
|
||||
ModalBody,
|
||||
ModalTitle,
|
||||
StoreLogoPreview,
|
||||
TimesIcon,
|
||||
AdvancedSettingsBox,
|
||||
EditStoreButtonsRow,
|
||||
CreateNewDataContainerRow,
|
||||
CreateNewDataContainerButton,
|
||||
} from "./CreateStoreModal-styles";
|
||||
import ImageUploader from "../common/ImageUploader";
|
||||
import {
|
||||
FilterSelect,
|
||||
FilterSelectMenuItems,
|
||||
@@ -45,121 +36,173 @@ import {
|
||||
FiltersOption,
|
||||
} from "../../pages/Store/Store/Store-styles";
|
||||
import { supportedCoinsArray } from "../../constants/supported-coins";
|
||||
import { QortalSVG } from "../../assets/svgs/QortalSVG";
|
||||
import { ARRRSVG } from "../../assets/svgs/ARRRSVG";
|
||||
import { coinPng } from "../../constants/coin-icons";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setIsLoadingGlobal, toggleEditStoreModal, setDataContainer, resetListProducts, resetProducts } from "../../state/features/globalSlice";
|
||||
import { setNotification } from "../../state/features/notificationsSlice";
|
||||
import { ReusableModal } from "./ReusableModal";
|
||||
import { DATA_CONTAINER_BASE, STORE_BASE } from "../../constants/identifiers";
|
||||
import { DATA_CONTAINER_BASE } from "../../constants/identifiers";
|
||||
import { objectToBase64 } from "../../utils/toBase64";
|
||||
import { ShortDataContainer } from "../../wrappers/GlobalWrapper";
|
||||
import { setNotification } from "../../state/features/notificationsSlice";
|
||||
|
||||
interface ForeignCoins {
|
||||
[key: string]: string;
|
||||
}
|
||||
type AnyObject = Record<string, any>;
|
||||
|
||||
export interface onPublishParamEdit {
|
||||
title: string;
|
||||
description: string;
|
||||
shipsTo: string;
|
||||
location: string;
|
||||
shipsTo: string;
|
||||
logo: string;
|
||||
foreignCoins: ForeignCoins;
|
||||
foreignCoins: Record<string, string>;
|
||||
supportedCoins: string[];
|
||||
}
|
||||
interface MyModalProps {
|
||||
|
||||
|
||||
interface EditStoreModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onPublish: (param: onPublishParamEdit) => Promise<void>;
|
||||
store: AnyObject;
|
||||
/** Back-compat: GlobalWrapper may pass onPublish instead of onUpdate */
|
||||
onPublish?: (param: onPublishParamEdit) => Promise<void>;
|
||||
onUpdate?: (param: onPublishParamEdit) => Promise<void>;
|
||||
onClose?: () => void;
|
||||
closeEditStoreModal?: boolean;
|
||||
setCloseEditStoreModal?: (val: boolean) => void;
|
||||
username: string;
|
||||
}
|
||||
|
||||
const MyModal: React.FC<MyModalProps> = ({ open, onClose, onPublish }) => {
|
||||
const EditStoreModal: React.FC<EditStoreModalProps> = ({
|
||||
open,
|
||||
store,
|
||||
onPublish,
|
||||
onUpdate,
|
||||
onClose,
|
||||
closeEditStoreModal,
|
||||
setCloseEditStoreModal,
|
||||
username,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const currentStore = useSelector(
|
||||
(state: RootState) => state.global.currentStore
|
||||
);
|
||||
const user = useSelector((state: RootState) => state.auth.user);
|
||||
|
||||
const storeId = useSelector((state: RootState) => state.store.storeId);
|
||||
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [location, setLocation] = useState<string>("");
|
||||
const [shipsTo, setShipsTo] = useState<string>("");
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [logo, setLogo] = useState<string | null>(null);
|
||||
const [supportedCoinsSelected, setSupportedCoinsSelected] = useState<
|
||||
string[]
|
||||
>(["QORT"]);
|
||||
const [qortWalletAddress, setQortWalletAddress] = useState<string>("");
|
||||
const [arrrWalletAddress, setArrrWalletAddress] = useState<string>("");
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] =
|
||||
useState<boolean>(false);
|
||||
const [showCreateNewDataContainerModal, setShowCreateNewDataContainerModal] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const handlePublish = async (): Promise<void> => {
|
||||
// Persist unsaved edits (so closing the modal/backdrop click won't discard)
|
||||
const STORAGE_KEY_EDIT = `qshop_edit_${store?.storeIdentifier || ""}`;
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState<boolean>(false);
|
||||
const [showCreateNewDataContainerModal, setShowCreateNewDataContainerModal] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
try {
|
||||
const cached = localStorage.getItem(STORAGE_KEY_EDIT);
|
||||
if (cached) {
|
||||
const parsed = JSON.parse(cached);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
if (parsed.title) setTitle(parsed.title);
|
||||
if (parsed.description) setDescription(parsed.description);
|
||||
if (parsed.location) setLocation(parsed.location);
|
||||
if (parsed.shipsTo) setShipsTo(parsed.shipsTo);
|
||||
if (parsed.logo) setLogo(parsed.logo);
|
||||
if (Array.isArray(parsed.supportedCoinsSelected)) setSupportedCoinsSelected(parsed.supportedCoinsSelected);
|
||||
if (parsed.foreignWallets && typeof parsed.foreignWallets === 'object') setForeignWallets(parsed.foreignWallets);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}, [open]);
|
||||
|
||||
// Seed from existing store (guard for undefined/null)
|
||||
const [title, setTitle] = useState<string>(store?.title || "");
|
||||
const [description, setDescription] = useState<string>(store?.description || "");
|
||||
const [location, setLocation] = useState<string>(store?.location || "");
|
||||
const [shipsTo, setShipsTo] = useState<string>(store?.shipsTo || "");
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [storeIdentifier, setStoreIdentifier] = useState<string>(store?.storeIdentifier || "");
|
||||
const [logo, setLogo] = useState<string | null>(store?.logo || null);
|
||||
const [supportedCoinsSelected, setSupportedCoinsSelected] = useState<string[]>(store?.supportedCoins || ["QORT"]);
|
||||
const [foreignWallets, setForeignWallets] = useState<Record<string, string>>(
|
||||
store?.foreignCoins || { ARRR: "" }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const payload = { title, description, location, shipsTo, logo, supportedCoinsSelected, foreignWallets };
|
||||
try { localStorage.setItem(STORAGE_KEY_EDIT, JSON.stringify(payload)); } catch {}
|
||||
}, [open, title, description, location, shipsTo, logo, supportedCoinsSelected, foreignWallets]);
|
||||
|
||||
useEffect(() => {
|
||||
// Keep fields in sync if a different store is loaded while open
|
||||
if (store) {
|
||||
setTitle(store.title || "");
|
||||
setDescription(store.description || "");
|
||||
setLocation(store.location || "");
|
||||
setShipsTo(store.shipsTo || "");
|
||||
setStoreIdentifier(store.storeIdentifier || "");
|
||||
setLogo(store.logo || null);
|
||||
setSupportedCoinsSelected(store.supportedCoins || ["QORT"]);
|
||||
setForeignWallets(store.foreignCoins || { ARRR: "" });
|
||||
}
|
||||
}, [store]);
|
||||
|
||||
const handleUpdate = async (): Promise<void> => {
|
||||
try {
|
||||
setErrorMessage("");
|
||||
if (!logo) {
|
||||
setErrorMessage("A logo is required");
|
||||
return;
|
||||
}
|
||||
|
||||
const foreignCoins: ForeignCoins = {
|
||||
ARRR: arrrWalletAddress,
|
||||
};
|
||||
const foreignCoins: Record<string, string> = {};
|
||||
supportedCoinsSelected
|
||||
.filter(coin => coin !== "QORT")
|
||||
.filter((coin) => coin !== "QORT")
|
||||
.forEach((item: string) => {
|
||||
if (!foreignCoins[item])
|
||||
throw new Error(`Please add a ${item} address`);
|
||||
const addr = foreignWallets[item]?.trim();
|
||||
if (!addr) throw new Error(`Please add a ${item} address`);
|
||||
foreignCoins[item] = addr;
|
||||
});
|
||||
await onPublish({
|
||||
|
||||
const save = onUpdate ?? onPublish;
|
||||
if (!save) {
|
||||
throw new Error("Save handler missing (onUpdate/onPublish).");
|
||||
}
|
||||
|
||||
const payload: onPublishParamEdit = {
|
||||
...store,
|
||||
title,
|
||||
description,
|
||||
shipsTo,
|
||||
location,
|
||||
logo,
|
||||
foreignCoins: {
|
||||
ARRR: arrrWalletAddress,
|
||||
},
|
||||
foreignCoins,
|
||||
supportedCoins: supportedCoinsSelected,
|
||||
});
|
||||
handleClose();
|
||||
};
|
||||
await save(payload);
|
||||
|
||||
setCloseEditStoreModal?.(true);
|
||||
onClose?.();
|
||||
} catch (error: any) {
|
||||
setErrorMessage(error.message);
|
||||
setErrorMessage(error?.message || String(error));
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChangeId = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
let newValue = event.target.value
|
||||
.replace(/[^a-zA-Z0-9\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.trim();
|
||||
|
||||
if (newValue.toLowerCase().includes("post")) {
|
||||
newValue = newValue.replace(/post/gi, "");
|
||||
}
|
||||
if (newValue.toLowerCase().includes("q-shop")) {
|
||||
newValue = newValue.replace(/q-shop/gi, "");
|
||||
}
|
||||
setStoreIdentifier(newValue);
|
||||
};
|
||||
|
||||
// Close modal if parent signals it
|
||||
useEffect(() => {
|
||||
if (open && currentStore && storeId === currentStore.id) {
|
||||
setTitle(currentStore?.title || "");
|
||||
setDescription(currentStore?.description || "");
|
||||
setLogo(currentStore?.logo || null);
|
||||
setLocation(currentStore?.location || "");
|
||||
setShipsTo(currentStore?.shipsTo || "");
|
||||
const selectedCoinsList = [...new Set([...(currentStore?.supportedCoins || []), 'QORT'])];
|
||||
setSupportedCoinsSelected(selectedCoinsList)
|
||||
setArrrWalletAddress(currentStore?.foreignCoins?.ARRR || "")
|
||||
if (closeEditStoreModal) {
|
||||
setCloseEditStoreModal?.(false);
|
||||
onClose?.();
|
||||
}
|
||||
}, [currentStore, storeId, open]);
|
||||
|
||||
const handleClose = (): void => {
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setErrorMessage("");
|
||||
setDescription("");
|
||||
setLogo(null);
|
||||
setLocation("");
|
||||
setShipsTo("");
|
||||
setArrrWalletAddress("");
|
||||
setSupportedCoinsSelected(["QORT"]);
|
||||
setShowAdvancedSettings(false);
|
||||
dispatch(toggleCreateStoreModal(false));
|
||||
onClose();
|
||||
};
|
||||
}, [closeEditStoreModal]);
|
||||
|
||||
const handleChipSelect = (value: string[]) => {
|
||||
setSupportedCoinsSelected(value);
|
||||
@@ -167,103 +210,89 @@ const MyModal: React.FC<MyModalProps> = ({ open, onClose, onPublish }) => {
|
||||
|
||||
const handleChipRemove = (chip: string) => {
|
||||
if (chip === "QORT") return;
|
||||
setSupportedCoinsSelected(prevChips => prevChips.filter(c => c !== chip));
|
||||
setSupportedCoinsSelected((prevChips) => prevChips.filter((c) => c !== chip));
|
||||
};
|
||||
|
||||
const importAddress = async (coin: string) => {
|
||||
try {
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
const res = await qortalRequest({
|
||||
action: "GET_USER_WALLET",
|
||||
coin,
|
||||
});
|
||||
if (res?.address) {
|
||||
setArrrWalletAddress(res.address);
|
||||
setForeignWallets((prev) => ({ ...prev, [coin]: res.address }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Show inline and global notification
|
||||
setErrorMessage(`Unable to import ${coin} address. Please insert it manually`);
|
||||
// Optional legacy notification
|
||||
// @ts-ignore
|
||||
setNotification && dispatch(setNotification({ alertType: "error", msg: `Unable to import ${coin} address. Please insert it manually` }));
|
||||
} finally {
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
}
|
||||
};
|
||||
|
||||
// Recreate Shop Data
|
||||
const handleClose = (): void => {
|
||||
setShowAdvancedSettings(false);
|
||||
if (setCloseEditStoreModal) setCloseEditStoreModal(true);
|
||||
// Ensure the modal actually closes
|
||||
dispatch(toggleEditStoreModal(false));
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleRecreateShopData = async () => {
|
||||
if (!currentStore || !user?.name) {
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: "Error! Missing shop data or name",
|
||||
alertType: "error",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const shortStoreId = currentStore?.shortStoreId;
|
||||
if (!store?.id || !username) {
|
||||
dispatch(setNotification({ msg: "Error! Missing shop data or name", alertType: "error" }));
|
||||
return;
|
||||
}
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
const shortStoreId = store?.shortStoreId || storeIdentifier || "";
|
||||
const dataContainer: ShortDataContainer = {
|
||||
storeId: currentStore.id,
|
||||
shortStoreId: shortStoreId,
|
||||
owner: user.name,
|
||||
storeId: store.id,
|
||||
shortStoreId,
|
||||
owner: username,
|
||||
products: {},
|
||||
};
|
||||
const dataContainerToBase64 = await objectToBase64(dataContainer);
|
||||
|
||||
const dataContainerCreated = await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: user?.name,
|
||||
name: username,
|
||||
service: "DOCUMENT",
|
||||
data64: dataContainerToBase64,
|
||||
identifier: `${currentStore.id}-${DATA_CONTAINER_BASE}`,
|
||||
identifier: `${store.id}-${DATA_CONTAINER_BASE}`,
|
||||
filename: "datacontainer.json",
|
||||
});
|
||||
if (dataContainerCreated && !dataContainerCreated.error) {
|
||||
dispatch(
|
||||
setDataContainer({
|
||||
...dataContainer,
|
||||
id: `${currentStore.id}-${DATA_CONTAINER_BASE}`,
|
||||
})
|
||||
);
|
||||
dispatch(setDataContainer({ ...dataContainer, id: `${store.id}-${DATA_CONTAINER_BASE}` } as any));
|
||||
dispatch(resetListProducts());
|
||||
dispatch(resetProducts());
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: "Data Container Created!",
|
||||
alertType: "success",
|
||||
})
|
||||
);
|
||||
onClose();
|
||||
dispatch(setNotification({ msg: "Data Container Created!", alertType: "success" }));
|
||||
setShowCreateNewDataContainerModal(false);
|
||||
setShowAdvancedSettings(false);
|
||||
} else {
|
||||
dispatch(setNotification({ msg: "Error creating data container", alertType: "error" }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: "Error when creating the data container. Please try again!",
|
||||
alertType: "error",
|
||||
})
|
||||
);
|
||||
dispatch(setNotification({ msg: "Error when creating data container", alertType: "error" }));
|
||||
} finally {
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="modal-title"
|
||||
aria-describedby="modal-description"
|
||||
>
|
||||
<>
|
||||
<Modal open={open} onClose={handleClose} aria-labelledby="modal-title" aria-describedby="modal-description">
|
||||
<ModalBody>
|
||||
<ModalTitle id="modal-title" variant="h6">
|
||||
Edit Shop
|
||||
</ModalTitle>
|
||||
<ModalTitle id="modal-title">Edit Shop</ModalTitle>
|
||||
|
||||
{!logo ? (
|
||||
<ImageUploader onPick={(img: string) => setLogo(img)}>
|
||||
<AddLogoButton>
|
||||
Add Shop Logo
|
||||
<AddLogoIcon
|
||||
sx={{
|
||||
height: "25px",
|
||||
width: "auto",
|
||||
}}
|
||||
></AddLogoIcon>
|
||||
<AddLogoIcon sx={{ height: "25px", width: "auto" }} />
|
||||
</AddLogoButton>
|
||||
</ImageUploader>
|
||||
) : (
|
||||
@@ -274,123 +303,101 @@ const MyModal: React.FC<MyModalProps> = ({ open, onClose, onPublish }) => {
|
||||
onClickFunc={() => setLogo(null)}
|
||||
height={"32"}
|
||||
width={"32"}
|
||||
></TimesIcon>
|
||||
/>
|
||||
</LogoPreviewRow>
|
||||
)}
|
||||
|
||||
<CustomInputField
|
||||
id="modal-url-preview"
|
||||
label="Url Preview"
|
||||
value={`/${username}/${storeIdentifier}`}
|
||||
fullWidth
|
||||
disabled
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
<CustomInputField
|
||||
id="modal-shopId-input"
|
||||
label="Shop Id"
|
||||
value={storeIdentifier}
|
||||
onChange={handleInputChangeId}
|
||||
fullWidth
|
||||
inputProps={{ maxLength: 25 }}
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
<CustomInputField
|
||||
id="modal-title-input"
|
||||
label="Title"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
inputProps={{ maxLength: 50 }}
|
||||
onChange={(e: any) => setTitle(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
inputProps={{ maxLength: 50 }}
|
||||
/>
|
||||
|
||||
<CustomInputField
|
||||
id="modal-description-input"
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
onChange={(e: any) => setDescription(e.target.value)}
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
<CustomInputField
|
||||
id="modal-location-input"
|
||||
label="Location"
|
||||
value={location}
|
||||
onChange={e => setLocation(e.target.value)}
|
||||
onChange={(e: any) => setLocation(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
<CustomInputField
|
||||
id="modal-shipsTo-input"
|
||||
label="Ships To"
|
||||
value={shipsTo}
|
||||
onChange={e => setShipsTo(e.target.value)}
|
||||
onChange={(e: any) => setShipsTo(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
{/* QORT Wallet Input Field */}
|
||||
{/* <WalletRow>
|
||||
<CustomInputField
|
||||
id="modal-qort-wallet-input"
|
||||
label="QORT Wallet Address"
|
||||
value={qortWalletAddress}
|
||||
onChange={(e: any) => {
|
||||
setQortWalletAddress(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
<Tooltip
|
||||
TransitionComponent={Zoom}
|
||||
placement="top"
|
||||
arrow={true} const importAddress = async (coin: string)=> {
|
||||
try {
|
||||
const res = await qortalRequest({
|
||||
action: 'GET_USER_WALLET',
|
||||
coin
|
||||
})
|
||||
if(res?.address){
|
||||
setArrrWalletAddress(res.address)
|
||||
}
|
||||
console.log({res})
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
title="Import your QORT Wallet Address from your current account"
|
||||
>
|
||||
<IconButton disableFocusRipple={true} disableRipple={true}>
|
||||
<DownloadArrrWalletIcon
|
||||
color={theme.palette.text.primary}
|
||||
height="40"
|
||||
width="40"
|
||||
{/* Foreign coin wallet inputs */}
|
||||
{supportedCoinsSelected
|
||||
.filter((coin) => coin !== "QORT")
|
||||
.map((coin) => (
|
||||
<WalletRow key={coin}>
|
||||
<CustomInputField
|
||||
id={`modal-wallet-${coin}`}
|
||||
label={`${coin} Wallet Address`}
|
||||
value={foreignWallets[coin] || ""}
|
||||
onChange={(e: any) => setForeignWallets((prev) => ({ ...prev, [coin]: e.target.value }))}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</WalletRow> */}
|
||||
<Tooltip
|
||||
TransitionComponent={Zoom}
|
||||
placement="top"
|
||||
arrow
|
||||
title={`Import your ${coin} Wallet Address from your current account`}
|
||||
>
|
||||
<IconButton disableFocusRipple disableRipple onClick={() => importAddress(coin)}>
|
||||
<DownloadArrrWalletIcon color={theme.palette.text.primary} height="40" width="40" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</WalletRow>
|
||||
))}
|
||||
|
||||
{/* ARRR Wallet Input Field */}
|
||||
<WalletRow>
|
||||
<CustomInputField
|
||||
id="modal-arrr-wallet-input"
|
||||
label="ARRR Wallet Address"
|
||||
value={arrrWalletAddress}
|
||||
onChange={(e: any) => {
|
||||
setArrrWalletAddress(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
<Tooltip
|
||||
TransitionComponent={Zoom}
|
||||
placement="top"
|
||||
arrow={true}
|
||||
title="Import your ARRR Wallet Address from your current account"
|
||||
>
|
||||
<IconButton
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}
|
||||
onClick={() => importAddress("ARRR")}
|
||||
>
|
||||
<DownloadArrrWalletIcon
|
||||
color={theme.palette.text.primary}
|
||||
height="40"
|
||||
width="40"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</WalletRow>
|
||||
{/* Coin selection available for your shop */}
|
||||
<FilterSelect
|
||||
disableClearable
|
||||
multiple
|
||||
@@ -403,45 +410,26 @@ const MyModal: React.FC<MyModalProps> = ({ open, onClose, onPublish }) => {
|
||||
handleChipSelect(value as string[]);
|
||||
}}
|
||||
renderTags={(values: any) =>
|
||||
values.map((value: string) => {
|
||||
return (
|
||||
<FiltersChip
|
||||
key={value}
|
||||
label={value}
|
||||
onDelete={
|
||||
value !== "QORT" ? () => handleChipRemove(value) : undefined
|
||||
}
|
||||
clickable={value === "QORT" ? false : true}
|
||||
/>
|
||||
);
|
||||
})
|
||||
values.map((value: string) => (
|
||||
<FiltersChip
|
||||
key={value}
|
||||
label={value}
|
||||
onDelete={value !== "QORT" ? () => handleChipRemove(value) : undefined}
|
||||
clickable={value === "QORT" ? false : true}
|
||||
/>
|
||||
))
|
||||
}
|
||||
renderOption={(props, option: any) => {
|
||||
const isDisabled = option === "QORT";
|
||||
return (
|
||||
<FiltersOption {...props}>
|
||||
<FiltersCheckbox
|
||||
disabled={isDisabled}
|
||||
checked={supportedCoinsSelected.some(coin => coin === option)}
|
||||
/>
|
||||
{option === "QORT" ? (
|
||||
<QortalSVG
|
||||
height="22"
|
||||
width="22"
|
||||
color={theme.palette.text.primary}
|
||||
/>
|
||||
) : option === "ARRR" ? (
|
||||
<ARRRSVG
|
||||
height="22"
|
||||
width="22"
|
||||
color={theme.palette.text.primary}
|
||||
/>
|
||||
) : null}
|
||||
<FiltersCheckbox disabled={isDisabled} checked={supportedCoinsSelected.some((coin) => coin === option)} />
|
||||
<img src={coinPng(option) || ""} alt={`${option}-logo`} width={22} height={22} />
|
||||
<span style={{ marginLeft: "5px" }}>{option}</span>
|
||||
</FiltersOption>
|
||||
);
|
||||
}}
|
||||
renderInput={params => (
|
||||
renderInput={(params) => (
|
||||
<FilterSelectMenuItems
|
||||
{...params}
|
||||
label="Supported Coins"
|
||||
@@ -449,23 +437,20 @@ const MyModal: React.FC<MyModalProps> = ({ open, onClose, onPublish }) => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{showAdvancedSettings && (
|
||||
<CreateNewDataContainerRow
|
||||
onClick={() => {
|
||||
setShowCreateNewDataContainerModal(true);
|
||||
}}
|
||||
>
|
||||
<CreateNewDataContainerButton>
|
||||
Recreate Shop Data
|
||||
</CreateNewDataContainerButton>
|
||||
</CreateNewDataContainerRow>
|
||||
)}
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}></FormControl>
|
||||
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }} />
|
||||
{errorMessage && (
|
||||
<Typography color="error" variant="body1">
|
||||
{errorMessage}
|
||||
</Typography>
|
||||
)}
|
||||
{showAdvancedSettings && (
|
||||
<CreateNewDataContainerRow onClick={() => setShowCreateNewDataContainerModal(true)}>
|
||||
<CreateNewDataContainerButton>
|
||||
Recreate Shop Data
|
||||
</CreateNewDataContainerButton>
|
||||
</CreateNewDataContainerRow>
|
||||
)}
|
||||
<ButtonRow sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<AdvancedSettingsBox>
|
||||
<Typography>Advanced Settings</Typography>
|
||||
@@ -475,58 +460,65 @@ const MyModal: React.FC<MyModalProps> = ({ open, onClose, onPublish }) => {
|
||||
/>
|
||||
</AdvancedSettingsBox>
|
||||
<EditStoreButtonsRow>
|
||||
<CancelButton
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<Tooltip title="Discard local edits (keeps saved store as-is)">
|
||||
<CancelButton
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
try { localStorage.removeItem(STORAGE_KEY_EDIT); } catch {}
|
||||
if (store) {
|
||||
setTitle(store.title || "");
|
||||
setDescription(store.description || "");
|
||||
setLocation(store.location || "");
|
||||
setShipsTo(store.shipsTo || "");
|
||||
setLogo(store.logo || null);
|
||||
setSupportedCoinsSelected(store.supportedCoins || ["QORT"]);
|
||||
setForeignWallets(store.foreignCoins || { ARRR: "" });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Clear Changes
|
||||
</CancelButton>
|
||||
</Tooltip>
|
||||
<CancelButton variant="outlined" color="error" onClick={handleClose}>
|
||||
Cancel
|
||||
</CancelButton>
|
||||
<CreateButton variant="contained" onClick={handlePublish}>
|
||||
Edit Shop
|
||||
<CreateButton variant="contained" onClick={handleUpdate}>
|
||||
Save Changes
|
||||
</CreateButton>
|
||||
</EditStoreButtonsRow>
|
||||
</ButtonRow>
|
||||
<ReusableModal
|
||||
open={showCreateNewDataContainerModal}
|
||||
customStyles={{
|
||||
width: "50%",
|
||||
maxWidth: 1700,
|
||||
height: "auto",
|
||||
backgroundColor:
|
||||
theme.palette.mode === "light" ? "#e8e8e8" : "#32333c",
|
||||
position: "relative",
|
||||
padding: "25px",
|
||||
borderRadius: "3px",
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
maxHeight: "90vh",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
Warning! ⚠️ Are you sure you want to recreate your shop's data? This
|
||||
will clear all your shop's products. This should only be done as a
|
||||
last resort if you cannot access your product manager or if you are
|
||||
experiencing issues products displaying properly.
|
||||
</Box>
|
||||
<ButtonRow>
|
||||
<CancelButton
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
setShowCreateNewDataContainerModal(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</CancelButton>
|
||||
<CreateButton variant="contained" onClick={handleRecreateShopData}>
|
||||
Recreate Shop Data
|
||||
</CreateButton>
|
||||
</ButtonRow>
|
||||
</ReusableModal>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
<ReusableModal
|
||||
open={showCreateNewDataContainerModal}
|
||||
onClose={() => setShowCreateNewDataContainerModal(false)}
|
||||
customStyles={{
|
||||
width: "50%",
|
||||
maxWidth: 1700,
|
||||
height: "auto",
|
||||
backgroundColor: theme.palette.mode === "light" ? "#e8e8e8" : "#32333c",
|
||||
position: "relative",
|
||||
padding: "25px",
|
||||
borderRadius: "3px",
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
maxHeight: "90vh",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
Warning! ⚠️ Recreating your shop data clears all products. Use only as a last resort.
|
||||
</div>
|
||||
<ButtonRow>
|
||||
<CancelButton variant="outlined" color="error" onClick={() => setShowCreateNewDataContainerModal(false)}>
|
||||
Cancel
|
||||
</CancelButton>
|
||||
<CreateNewDataContainerButton onClick={handleRecreateShopData}>
|
||||
Recreate Shop Data
|
||||
</CreateNewDataContainerButton>
|
||||
</ButtonRow>
|
||||
</ReusableModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyModal;
|
||||
export default EditStoreModal;
|
||||
|
||||
28
src/constants/coin-icons.ts
Normal file
28
src/constants/coin-icons.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// src/constants/coin-icons.ts
|
||||
// Central mapping for coin PNG assets used across the app.
|
||||
// All keys are UPPERCASE tickers.
|
||||
import QORT from "../assets/img/qort.png";
|
||||
import ARRR from "../assets/img/arrr.png";
|
||||
import BTC from "../assets/img/btc.png";
|
||||
import LTC from "../assets/img/ltc.png";
|
||||
import DOGE from "../assets/img/doge.png";
|
||||
import DGB from "../assets/img/dgb.png";
|
||||
import RVN from "../assets/img/rvn.png";
|
||||
|
||||
export type CoinTicker = "QORT" | "ARRR" | "BTC" | "LTC" | "DOGE" | "DGB" | "RVN";
|
||||
|
||||
export const CoinIcons: Record<string, string> = {
|
||||
QORT,
|
||||
ARRR,
|
||||
BTC,
|
||||
LTC,
|
||||
DOGE,
|
||||
DGB,
|
||||
RVN,
|
||||
};
|
||||
|
||||
/** Return PNG path for a coin ticker (case-insensitive). */
|
||||
export const coinPng = (coin: string | CoinTicker): string | undefined => {
|
||||
const key = String(coin || "").toUpperCase();
|
||||
return CoinIcons[key];
|
||||
};
|
||||
103
src/constants/coin-registry.ts
Normal file
103
src/constants/coin-registry.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
// src/constants/coin-registry.ts
|
||||
export type CoinTicker = 'QORT' | 'ARRR' | 'BTC' | 'LTC' | 'DOGE' | 'DGB' | 'RVN';
|
||||
|
||||
export interface CoinMeta {
|
||||
ticker: CoinTicker;
|
||||
decimals: number;
|
||||
/** Non-QORT UTXO coins */
|
||||
isUtxo: boolean;
|
||||
/** Whether the UI should offer an optional fee input during send */
|
||||
supportsCustomFee: boolean;
|
||||
/** Optional UI-only suggested fee range (coin units) */
|
||||
feeHintMin?: number;
|
||||
feeHintMax?: number;
|
||||
/** Build a transaction explorer URL for a given txid/signature */
|
||||
txExplorer?: (id: string) => string;
|
||||
/** Build an address explorer URL for a given address (for seller addr proof if needed) */
|
||||
addressExplorer?: (address: string) => string;
|
||||
}
|
||||
|
||||
export const CoinRegistry: Record<CoinTicker, CoinMeta> = {
|
||||
QORT: {
|
||||
ticker: 'QORT',
|
||||
decimals: 8,
|
||||
isUtxo: false,
|
||||
supportsCustomFee: false,
|
||||
txExplorer: (sig: string) => `https://explorer.qortal.org/transaction/${encodeURIComponent(sig)}`,
|
||||
addressExplorer: (addr: string) => `https://explorer.qortal.org/address/${encodeURIComponent(addr)}`,
|
||||
},
|
||||
ARRR: {
|
||||
ticker: 'ARRR',
|
||||
decimals: 8,
|
||||
isUtxo: true,
|
||||
supportsCustomFee: false, // keep off until tested
|
||||
txExplorer: (txid: string) => `https://explorer.pirate.black/tx/${encodeURIComponent(txid)}`,
|
||||
addressExplorer: (a: string) => `https://explorer.pirate.black/address/${encodeURIComponent(a)}`,
|
||||
},
|
||||
BTC: {
|
||||
ticker: 'BTC',
|
||||
decimals: 8,
|
||||
isUtxo: true,
|
||||
supportsCustomFee: true,
|
||||
feeHintMin: 0.000005,
|
||||
feeHintMax: 0.001,
|
||||
txExplorer: (txid: string) => `https://mempool.space/tx/${encodeURIComponent(txid)}`,
|
||||
addressExplorer: (a: string) => `https://mempool.space/address/${encodeURIComponent(a)}`,
|
||||
},
|
||||
LTC: {
|
||||
ticker: 'LTC',
|
||||
decimals: 8,
|
||||
isUtxo: true,
|
||||
supportsCustomFee: true,
|
||||
feeHintMin: 0.00001,
|
||||
feeHintMax: 0.01,
|
||||
txExplorer: (txid: string) => `https://litecoinspace.org/tx/${encodeURIComponent(txid)}`,
|
||||
addressExplorer: (a: string) => `https://litecoinspace.org/address/${encodeURIComponent(a)}`,
|
||||
},
|
||||
DOGE: {
|
||||
ticker: 'DOGE',
|
||||
decimals: 8,
|
||||
isUtxo: true,
|
||||
supportsCustomFee: true,
|
||||
feeHintMin: 0.1,
|
||||
feeHintMax: 5,
|
||||
txExplorer: (txid: string) => `https://dogechain.info/tx/${encodeURIComponent(txid)}`,
|
||||
addressExplorer: (a: string) => `https://dogechain.info/address/${encodeURIComponent(a)}`,
|
||||
},
|
||||
DGB: {
|
||||
ticker: 'DGB',
|
||||
decimals: 8,
|
||||
isUtxo: true,
|
||||
supportsCustomFee: true,
|
||||
feeHintMin: 0.0001,
|
||||
feeHintMax: 0.1,
|
||||
txExplorer: (txid: string) => `https://digiexplorer.info/tx/${encodeURIComponent(txid)}`,
|
||||
addressExplorer: (a: string) => `https://digiexplorer.info/address/${encodeURIComponent(a)}`,
|
||||
},
|
||||
RVN: {
|
||||
ticker: 'RVN',
|
||||
decimals: 8,
|
||||
isUtxo: true,
|
||||
supportsCustomFee: true,
|
||||
feeHintMin: 0.001,
|
||||
feeHintMax: 0.1,
|
||||
txExplorer: (txid: string) => `https://ravencoin.network/tx/${encodeURIComponent(txid)}`,
|
||||
addressExplorer: (a: string) => `https://ravencoin.network/address/${encodeURIComponent(a)}`,
|
||||
},
|
||||
};
|
||||
|
||||
export const isUtxo = (coin: CoinTicker) => CoinRegistry[coin].isUtxo;
|
||||
export const supportsCustomFee = (coin: CoinTicker) => CoinRegistry[coin].supportsCustomFee;
|
||||
export const txExplorerUrl = (coin: CoinTicker, id?: string | null) =>
|
||||
id ? CoinRegistry[coin].txExplorer?.(id) ?? null : null;
|
||||
export const addressExplorerUrl = (coin: CoinTicker, a?: string | null) =>
|
||||
a ? CoinRegistry[coin].addressExplorer?.(a) ?? null : null;
|
||||
|
||||
|
||||
export const feeHintRange = (coin: CoinTicker): {min: number, max: number} | null => {
|
||||
const meta = CoinRegistry[coin];
|
||||
if (meta?.feeHintMin != null && meta?.feeHintMax != null) {
|
||||
return { min: Number(meta.feeHintMin), max: Number(meta.feeHintMax) };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -1,8 +1,2 @@
|
||||
|
||||
enum SupportedCoins {
|
||||
ARRR = 'ARRR',
|
||||
QORT = 'QORT',
|
||||
}
|
||||
|
||||
export const supportedCoinsArray: string[] = Object.values(SupportedCoins);
|
||||
|
||||
export const supportedCoinsArray = ['QORT','ARRR','BTC','LTC','DOGE','DGB','RVN'] as const;
|
||||
export type SupportedCoin = typeof supportedCoinsArray[number];
|
||||
|
||||
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@@ -29,6 +29,7 @@ interface QortalRequestOptions {
|
||||
coin?: string
|
||||
destinationAddress?: string
|
||||
amount?: number
|
||||
fee?: string | number
|
||||
blob?: Blob
|
||||
mimeType?: string
|
||||
file?: File
|
||||
|
||||
27
src/lib/coins.ts
Normal file
27
src/lib/coins.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
export type CoinSymbol = 'QORT' | 'ARRR' | 'BTC' | 'LTC' | 'DOGE' | 'DGB' | 'RVN';
|
||||
|
||||
export type CoinMeta = {
|
||||
symbol: CoinSymbol;
|
||||
display: string;
|
||||
isNative: boolean; // QORT = native
|
||||
priceKey?: string; // used for /crosschain/price/{priceKey}?inverse=true
|
||||
supportsCustomFee?: boolean; // allow user-specified fee at checkout
|
||||
};
|
||||
|
||||
export const COINS: Record<CoinSymbol, CoinMeta> = {
|
||||
QORT: { symbol: 'QORT', display: 'Qortal', isNative: true, priceKey: 'QORT', supportsCustomFee: false },
|
||||
ARRR: { symbol: 'ARRR', display: 'Pirate Chain', isNative: false, priceKey: 'PIRATECHAIN', supportsCustomFee: false },
|
||||
BTC: { symbol: 'BTC', display: 'Bitcoin', isNative: false, priceKey: 'BITCOIN', supportsCustomFee: true },
|
||||
LTC: { symbol: 'LTC', display: 'Litecoin', isNative: false, priceKey: 'LITECOIN', supportsCustomFee: true },
|
||||
DOGE: { symbol: 'DOGE', display: 'Dogecoin', isNative: false, priceKey: 'DOGECOIN', supportsCustomFee: true },
|
||||
DGB: { symbol: 'DGB', display: 'DigiByte', isNative: false, priceKey: 'DIGIBYTE', supportsCustomFee: true },
|
||||
RVN: { symbol: 'RVN', display: 'Ravencoin', isNative: false, priceKey: 'RAVENCOIN', supportsCustomFee: true },
|
||||
};
|
||||
|
||||
export const isNative = (coin: string) => coin === 'QORT';
|
||||
export const isForeign = (coin: string) => !isNative(coin as CoinSymbol);
|
||||
export const supportsCustomFee = (coin: string) => {
|
||||
const meta = COINS[coin as CoinSymbol];
|
||||
return !!meta?.supportsCustomFee;
|
||||
};
|
||||
20
src/lib/explorers.ts
Normal file
20
src/lib/explorers.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// src/lib/explorers.ts
|
||||
import { CoinRegistry, CoinTicker } from "../constants/coin-registry";
|
||||
|
||||
export const explorerTxUrl = (ticker: string | CoinTicker, id: string): string | undefined => {
|
||||
const key = String(ticker || "").toUpperCase() as CoinTicker;
|
||||
const meta = (CoinRegistry as any)[key];
|
||||
if (meta && typeof meta.txExplorer === "function" && id) {
|
||||
try { return meta.txExplorer(id); } catch {}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const explorerAddressUrl = (ticker: string | CoinTicker, addr: string): string | undefined => {
|
||||
const key = String(ticker || "").toUpperCase() as CoinTicker;
|
||||
const meta = (CoinRegistry as any)[key];
|
||||
if (meta && typeof meta.addressExplorer === "function" && addr) {
|
||||
try { return meta.addressExplorer(addr); } catch {}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
22
src/lib/pricing.ts
Normal file
22
src/lib/pricing.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
import { COINS } from './coins';
|
||||
|
||||
/**
|
||||
* Returns a numeric exchange ratio representing how many {coin} per 1 QORT,
|
||||
* mirroring prior ARRR behavior which used ?inverse=true and divided by 1e8.
|
||||
* Returns null if unavailable.
|
||||
*/
|
||||
export async function getPriceHint(coin: string): Promise<number | null> {
|
||||
try {
|
||||
const meta = COINS[coin as keyof typeof COINS];
|
||||
if (!meta || !meta.priceKey || coin === 'QORT') return null;
|
||||
const url = `/crosschain/price/${meta.priceKey}?maxtrades=10&inverse=true`;
|
||||
const resp = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' }});
|
||||
const txt = await resp.text();
|
||||
const ratio = (+txt) / 100000000;
|
||||
if (isNaN(ratio)) throw new Error('Cannot get exchange rate');
|
||||
return ratio;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
259
src/lib/validation.ts
Normal file
259
src/lib/validation.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { bech32, bech32m, base58check as _base58check } from '@scure/base';
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
|
||||
/** Result type for address validation */
|
||||
const b58check = _base58check(sha256);
|
||||
|
||||
export type ValidationResult = {
|
||||
valid: boolean;
|
||||
type?: string;
|
||||
variant?: 'base58' | 'bech32' | 'bech32m';
|
||||
hrp?: string;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
/** ---------- Base58 helpers ---------- */
|
||||
function tryBase58(addr: string, allowedVersions: number[]): ValidationResult | null {
|
||||
try {
|
||||
const decoded = b58check.decode(addr);
|
||||
const version = decoded[0];
|
||||
if (!allowedVersions.includes(version)) {
|
||||
return { valid: false, variant: 'base58', reason: `unexpected version byte ${version}` };
|
||||
}
|
||||
const payloadLen = decoded.length - 1;
|
||||
if (payloadLen !== 20)
|
||||
return { valid: false, variant: 'base58', reason: `unexpected payload length ${payloadLen}` };
|
||||
return { valid: true, variant: 'base58' };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** ---------- Bech32 / Bech32m helpers ---------- */
|
||||
function tryBech(addr: string, expectedHrps: string[]): ValidationResult | null {
|
||||
const a = addr.trim();
|
||||
if (!a) return { valid: false, reason: 'empty' };
|
||||
const lower = a.toLowerCase();
|
||||
const hrp = lower.split('1', 1)[0] || '';
|
||||
if (!expectedHrps.includes(hrp)) return null;
|
||||
|
||||
let decOk: any = null;
|
||||
let decmOk: any = null;
|
||||
|
||||
try {
|
||||
decOk = bech32.decode(lower as `${string}1${string}`);
|
||||
} catch (e) {
|
||||
// ignore decode errors; we'll try bech32m or return null
|
||||
}
|
||||
try {
|
||||
decmOk = bech32m.decode(lower as `${string}1${string}`);
|
||||
} catch (e) {
|
||||
// ignore decode errors; handled by returning null below
|
||||
}
|
||||
|
||||
// Prefer a valid verdict over invalid; evaluate both
|
||||
if (decOk) {
|
||||
const words = decOk.words;
|
||||
if (words.length >= 1) {
|
||||
const witver = words[0];
|
||||
if (witver === 0) {
|
||||
const program = bech32.fromWords(words.slice(1));
|
||||
if (program.length === 20 || program.length === 32) {
|
||||
return {
|
||||
valid: true,
|
||||
variant: 'bech32',
|
||||
hrp: decOk.prefix,
|
||||
type: program.length === 20 ? 'p2wpkh' : 'p2wsh',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (decmOk) {
|
||||
const words = decmOk.words;
|
||||
if (words.length >= 1) {
|
||||
const witver = words[0];
|
||||
if (witver !== 0) {
|
||||
const program = bech32m.fromWords(words.slice(1));
|
||||
if (program.length >= 2 && program.length <= 40) {
|
||||
return {
|
||||
valid: true,
|
||||
variant: 'bech32m',
|
||||
hrp: decmOk.prefix,
|
||||
type: witver === 1 && program.length === 32 ? 'taproot' : `witver-${witver}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (decOk) return { valid: false, variant: 'bech32', hrp, reason: 'invalid bech32 witness data' };
|
||||
if (decmOk)
|
||||
return { valid: false, variant: 'bech32m', hrp, reason: 'invalid bech32m witness data' };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** ---------- Public validators ---------- */
|
||||
export function validateBitcoinAddress(
|
||||
addr: string,
|
||||
net: 'mainnet' | 'testnet' = 'mainnet',
|
||||
): ValidationResult {
|
||||
const a = addr?.trim() ?? '';
|
||||
if (!a) return { valid: false, reason: 'empty' };
|
||||
// Base58
|
||||
const base = tryBase58(a, net === 'mainnet' ? [0x00, 0x05] : [0x6f, 0xc4]); // p2pkh, p2sh
|
||||
if (base) return base;
|
||||
// Bech
|
||||
const bech = tryBech(a, net === 'mainnet' ? ['bc'] : ['tb']);
|
||||
if (bech) return bech;
|
||||
return { valid: false, reason: 'unrecognized format' };
|
||||
}
|
||||
|
||||
export function validateLitecoinAddress(
|
||||
addr: string,
|
||||
net: 'mainnet' | 'testnet' = 'mainnet',
|
||||
): ValidationResult {
|
||||
const a = addr?.trim() ?? '';
|
||||
if (!a) return { valid: false, reason: 'empty' };
|
||||
// Base58: L (0x30), M (0x32), and legacy 3 (0x05)
|
||||
const base = tryBase58(a, net === 'mainnet' ? [0x30, 0x32, 0x05] : [0x6f, 0x3a]); // testnet p2pkh 0x6f, p2sh 0x3a
|
||||
if (base) return base;
|
||||
// Bech32 HRPs for Litecoin: ltc, tltc; also MWEB: ltcmweb, tltcmweb (treat as bech32(m))
|
||||
const hrps = net === 'mainnet' ? ['ltc', 'ltcmweb'] : ['tltc', 'tltcmweb'];
|
||||
const bech = tryBech(a, hrps);
|
||||
if (bech) return bech;
|
||||
return { valid: false, reason: 'unrecognized format' };
|
||||
}
|
||||
|
||||
/** Qortal address validation through Core API */
|
||||
export async function validateQortalAddress(addr: string): Promise<ValidationResult> {
|
||||
const a = addr?.trim() ?? '';
|
||||
if (!a) return { valid: false, reason: 'empty' };
|
||||
try {
|
||||
const res = await fetch(`/addresses/validate/${encodeURIComponent(a)}`);
|
||||
if (!res.ok) return { valid: false, reason: `HTTP ${res.status}` };
|
||||
const data = await res.json();
|
||||
if (typeof data?.isValid === 'boolean') {
|
||||
return { valid: data.isValid, reason: data.message };
|
||||
}
|
||||
return { valid: false, reason: 'unexpected API response' };
|
||||
} catch (e: any) {
|
||||
return { valid: false, reason: e?.message || 'network error' };
|
||||
}
|
||||
}
|
||||
|
||||
/** Coin router convenience */
|
||||
export function validateAddress(
|
||||
coin: string,
|
||||
addr: string,
|
||||
net: 'mainnet' | 'testnet' = 'mainnet',
|
||||
): ValidationResult | Promise<ValidationResult> {
|
||||
switch (coin) {
|
||||
case 'BTC':
|
||||
return validateBitcoinAddress(addr, net);
|
||||
case 'LTC':
|
||||
return validateLitecoinAddress(addr, net);
|
||||
case 'DOGE':
|
||||
return validateDogecoinAddress(addr, net);
|
||||
case 'DGB':
|
||||
return validateDigibyteAddress(addr, net);
|
||||
case 'RVN':
|
||||
return validateRavencoinAddress(addr, net);
|
||||
case 'QORT':
|
||||
return validateQortalAddress(addr);
|
||||
case 'ARRR':
|
||||
return validateArrrAddress(addr, net);
|
||||
default:
|
||||
return { valid: false, reason: 'unsupported coin' };
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert Litecoin 'M...' P2SH (0x32) to legacy '3...' P2SH (0x05) for Core compatibility */
|
||||
export function normalizeLitecoinAddressForSend(addr: string): {
|
||||
normalized?: string;
|
||||
ok: boolean;
|
||||
reason?: string;
|
||||
} {
|
||||
const a = addr?.trim() ?? '';
|
||||
if (!a) return { ok: false, reason: 'empty' };
|
||||
if (/^(ltc1|tltc1)/i.test(a) || a.startsWith('3')) return { ok: true, normalized: a };
|
||||
try {
|
||||
const decoded = b58check.decode(a);
|
||||
const version = decoded[0];
|
||||
if (version === 0x32) {
|
||||
decoded[0] = 0x05;
|
||||
return { ok: true, normalized: b58check.encode(decoded) };
|
||||
}
|
||||
return { ok: true, normalized: a };
|
||||
} catch {
|
||||
return { ok: false, reason: 'invalid base58' };
|
||||
}
|
||||
}
|
||||
|
||||
export function validateDigibyteAddress(
|
||||
addr: string,
|
||||
net: 'mainnet' | 'testnet' = 'mainnet',
|
||||
): ValidationResult {
|
||||
const a = addr?.trim() ?? '';
|
||||
if (!a) return { valid: false, reason: 'empty' };
|
||||
// Base58: D (0x1e) for P2PKH; P2SH new 'S' (0x3f) and legacy '3' (0x05)
|
||||
const allowed = net === 'mainnet' ? [0x1e, 0x3f, 0x05] : [0x7e, 0x3f];
|
||||
const base = tryBase58(a, allowed);
|
||||
if (base) return base;
|
||||
// Bech32 HRPs: dgb (mainnet), tdgb (testnet)
|
||||
const hrps = net === 'mainnet' ? ['dgb'] : ['tdgb'];
|
||||
const bech = tryBech(a, hrps);
|
||||
if (bech) return bech;
|
||||
return { valid: false, reason: 'unrecognized format' };
|
||||
}
|
||||
|
||||
export function validateDogecoinAddress(
|
||||
addr: string,
|
||||
net: 'mainnet' | 'testnet' = 'mainnet',
|
||||
): ValidationResult {
|
||||
const a = addr?.trim() ?? '';
|
||||
if (!a) return { valid: false, reason: 'empty' };
|
||||
// Base58 mainnet: P2PKH D (0x1e), P2SH 9/A (0x16). Testnet roughly n (0x6f), 2 (0xc4).
|
||||
const allowed = net === 'mainnet' ? [0x1e, 0x16] : [0x6f, 0xc4];
|
||||
const base = tryBase58(a, allowed);
|
||||
if (base) return base;
|
||||
// As of now, Dogecoin does not use bech32 for payments on mainnet.
|
||||
return { valid: false, reason: 'unrecognized format' };
|
||||
}
|
||||
|
||||
export function validateRavencoinAddress(
|
||||
addr: string,
|
||||
net: 'mainnet' | 'testnet' = 'mainnet',
|
||||
): ValidationResult {
|
||||
const a = addr?.trim() ?? '';
|
||||
if (!a) return { valid: false, reason: 'empty' };
|
||||
// Base58 mainnet: P2PKH R (0x3c), P2SH r (0x7a)
|
||||
const allowed = net === 'mainnet' ? [0x3c, 0x7a] : [0x6f, 0xc4];
|
||||
const base = tryBase58(a, allowed);
|
||||
if (base) return base;
|
||||
return { valid: false, reason: 'unrecognized format' };
|
||||
}
|
||||
|
||||
export function validateArrrAddress(
|
||||
addr: string,
|
||||
net: 'mainnet' | 'testnet' = 'mainnet',
|
||||
): ValidationResult {
|
||||
const a = addr?.trim() ?? '';
|
||||
if (!a) return { valid: false, reason: 'empty' };
|
||||
// ARRR Sapling bech32: HRP 'zs' on mainnet, 43-byte payload (~78 chars). Lowercase and ensure it contains the separator '1'.
|
||||
try {
|
||||
const s = a.toLowerCase();
|
||||
if (!s.includes('1')) throw new Error('not bech32');
|
||||
const { prefix, words } = bech32.decode(s as `${string}1${string}`);
|
||||
if (prefix !== 'zs') return { valid: false, reason: 'wrong HRP (expected zs)' };
|
||||
const bytes = new Uint8Array(bech32.fromWords(words));
|
||||
if (bytes.length !== 43)
|
||||
return {
|
||||
valid: false,
|
||||
variant: 'bech32',
|
||||
reason: `unexpected payload length ${bytes.length}`,
|
||||
};
|
||||
return { valid: true, variant: 'bech32' };
|
||||
} catch {
|
||||
return { valid: false, reason: 'unrecognized format' };
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,9 @@ import {
|
||||
useMediaQuery,
|
||||
FormControl,
|
||||
SelectChangeEvent,
|
||||
Box,
|
||||
|
||||
} from "@mui/material";
|
||||
import { TextField } from "@mui/material";
|
||||
import {
|
||||
addQuantityToCart,
|
||||
subtractQuantityFromCart,
|
||||
@@ -81,6 +82,8 @@ import { AcceptedCoin } from "../StoreList/StoreList-styles";
|
||||
import { ARRRSVG } from "../../assets/svgs/ARRRSVG";
|
||||
import { setPreferredCoin } from "../../state/features/storeSlice";
|
||||
import { CoinFilter } from "../Store/Store/Store";
|
||||
import { supportsCustomFee } from "../../lib/coins";
|
||||
import { feeHintRange } from "../../constants/coin-registry";
|
||||
import { useModal } from "../../components/common/useModal";
|
||||
import { MultiplePublish } from "../../components/common/MultiplePublish/MultiplePublish";
|
||||
|
||||
@@ -140,6 +143,7 @@ export const Cart = () => {
|
||||
const [deliveryNote, setDeliveryNote] = useState<string>("");
|
||||
const [confirmPurchaseModalOpen, setConfirmPurchaseModalOpen] =
|
||||
useState<boolean>(false);
|
||||
const [customFee, setCustomFee] = useState<string>("");
|
||||
const preferredCoin = useSelector((state: RootState) => state.store.preferredCoin);
|
||||
const [exchangeRate, setExchangeRate] = useState<number | null>(
|
||||
null
|
||||
@@ -183,11 +187,8 @@ export const Cart = () => {
|
||||
|
||||
const switchCoin = async ()=> {
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
|
||||
await calculateARRRExchangeRate()
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
|
||||
|
||||
}
|
||||
|
||||
useEffect(()=> {
|
||||
@@ -196,6 +197,7 @@ export const Cart = () => {
|
||||
}
|
||||
}, [preferredCoin, storeToUse])
|
||||
|
||||
|
||||
const coinToUse = useMemo(()=> {
|
||||
if(preferredCoin === CoinFilter.arrr && storeToUse?.supportedCoins?.includes(CoinFilter.arrr)){
|
||||
return CoinFilter.arrr
|
||||
@@ -204,6 +206,12 @@ export const Cart = () => {
|
||||
}
|
||||
}, [preferredCoin, storeToUse])
|
||||
|
||||
// Fee hints derived from registry for the selected coin
|
||||
const feeHint = useMemo(() => feeHintRange(String(coinToUse).toUpperCase() as any), [coinToUse]);
|
||||
const feeHelper: string | undefined = feeHint ? `Suggested: ${feeHint.min}–${feeHint.max} ${String(coinToUse)}` : undefined;
|
||||
const feeOutOfRange: boolean = !!feeHint && customFee !== "" && (Number(customFee) < (feeHint as any).min || Number(customFee) > (feeHint as any).max);
|
||||
|
||||
|
||||
// Set cart & orders to local state
|
||||
useEffect(() => {
|
||||
if (storeId && Object.keys(carts).length > 0) {
|
||||
@@ -382,8 +390,9 @@ export const Cart = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
let responseSendCoin = null
|
||||
let signature = null
|
||||
let responseSendCoin: any = null
|
||||
let signature: string | null = null
|
||||
let txId: string | null = null
|
||||
|
||||
if(coinToUse === CoinFilter.qort){
|
||||
responseSendCoin = await qortalRequest({
|
||||
@@ -392,24 +401,27 @@ export const Cart = () => {
|
||||
destinationAddress: address,
|
||||
amount: priceToPay,
|
||||
});
|
||||
signature = responseSendCoin.signature;
|
||||
signature = responseSendCoin?.signature ?? responseSendCoin?.data?.signature ?? null;
|
||||
|
||||
} else if(coinToUse === CoinFilter.arrr){
|
||||
|
||||
if(!storeToUse?.foreignCoins?.ARRR) throw new Error('Store has not set an ARRR address')
|
||||
responseSendCoin = await qortalRequest({
|
||||
action: "SEND_COIN",
|
||||
coin: "ARRR",
|
||||
destinationAddress: storeToUse?.foreignCoins?.ARRR.trim(),
|
||||
amount: priceToPay,
|
||||
});
|
||||
} else {
|
||||
dispatch(
|
||||
setNotification({
|
||||
alertType: "error",
|
||||
msg: "Currency not found",
|
||||
})
|
||||
);
|
||||
const coinTicker = String(coinToUse).toUpperCase();
|
||||
if (!storeToUse?.foreignCoins?.[coinTicker]) throw new Error(`Store has not set a ${coinTicker} address`);
|
||||
const dest = storeToUse?.foreignCoins?.[coinTicker].trim();
|
||||
const sendParams: any = {
|
||||
action: 'SEND_COIN',
|
||||
coin: coinTicker,
|
||||
destinationAddress: dest,
|
||||
amount: priceToPay,
|
||||
};
|
||||
if (coinTicker !== 'QORT' && coinTicker !== 'ARRR' && customFee) {
|
||||
sendParams.fee = customFee;
|
||||
}
|
||||
responseSendCoin = await qortalRequest(sendParams);
|
||||
if (coinTicker === 'QORT') {
|
||||
signature = responseSendCoin?.signature ?? responseSendCoin?.data?.signature ?? null;
|
||||
} else {
|
||||
txId = (responseSendCoin && (responseSendCoin.txId || responseSendCoin.transactionId || (responseSendCoin.data && responseSendCoin.data.txId))) || null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -435,8 +447,10 @@ export const Cart = () => {
|
||||
payment: {
|
||||
total: priceToPay,
|
||||
currency: coinToUse,
|
||||
transactionSignature: signature,
|
||||
arrrAddressUsed: coinToUse === CoinFilter.arrr ? storeToUse?.foreignCoins?.ARRR.trim() : ""
|
||||
transactionSignature: signature || "",
|
||||
addressUsed: coinToUse !== CoinFilter.qort ? (storeToUse?.foreignCoins?.[coinToUse]?.trim() || "") : "",
|
||||
arrrAddressUsed: coinToUse === CoinFilter.arrr ? storeToUse?.foreignCoins?.ARRR.trim() : "",
|
||||
txId: coinToUse !== CoinFilter.qort ? (txId || "") : ""
|
||||
},
|
||||
communicationMethod: ["Q-Mail"],
|
||||
};
|
||||
@@ -460,8 +474,10 @@ export const Cart = () => {
|
||||
payment: {
|
||||
total: priceToPay,
|
||||
currency: coinToUse,
|
||||
transactionSignature: signature,
|
||||
arrrAddressUsed: coinToUse === CoinFilter.arrr ? storeToUse?.foreignCoins?.ARRR.trim() : ""
|
||||
transactionSignature: signature || "",
|
||||
addressUsed: coinToUse !== CoinFilter.qort ? (storeToUse?.foreignCoins?.[coinToUse]?.trim() || "") : "",
|
||||
arrrAddressUsed: coinToUse === CoinFilter.arrr ? storeToUse?.foreignCoins?.ARRR.trim() : "",
|
||||
txId: coinToUse !== CoinFilter.qort ? (txId || "") : ""
|
||||
},
|
||||
communicationMethod: ["Q-Mail"],
|
||||
};
|
||||
@@ -1356,6 +1372,20 @@ export const Cart = () => {
|
||||
<ConfirmPurchaseRow>
|
||||
Are you sure you wish to complete this purchase?
|
||||
</ConfirmPurchaseRow>
|
||||
<ConfirmPurchaseRow>
|
||||
{!(coinToUse === CoinFilter.qort || coinToUse === CoinFilter.arrr) && supportsCustomFee(coinToUse) && (
|
||||
<TextField
|
||||
label="Network fee (optional)"
|
||||
helperText={feeHelper}
|
||||
error={feeOutOfRange}
|
||||
placeholder="e.g. 0.0002"
|
||||
value={customFee}
|
||||
onChange={(e) => setCustomFee(e.target.value)}
|
||||
variant="filled"
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
</ConfirmPurchaseRow>
|
||||
<ConfirmPurchaseRow style={{ gap: "15px" }}>
|
||||
<CancelButton
|
||||
variant="outlined"
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Order } from "../../../state/features/orderSlice";
|
||||
import { CircularProgress } from "@mui/material";
|
||||
import { StyledTableRow } from "./OrderTable-styles";
|
||||
import { setNotification } from "../../../state/features/notificationsSlice";
|
||||
import PaymentProof from "../../../components/common/PaymentProof";
|
||||
|
||||
const tableCellFontSize = "16px";
|
||||
|
||||
@@ -154,6 +155,12 @@ export const OrderTable = ({
|
||||
processedOrders.map(({ index, rowData }) => (
|
||||
<StyledTableRow key={index}>
|
||||
{rowContent(index, rowData, openOrder)}
|
||||
<TableCell sx={{ whiteSpace: 'nowrap' }}>
|
||||
<PaymentProof
|
||||
coin={(rowData?.payment?.currency === 'qort' ? 'QORT' : String(rowData?.payment?.currency || '').toUpperCase()) as any}
|
||||
proof={((rowData?.payment?.currency === 'qort') ? (rowData as any)?.payment?.transactionSignature : (rowData as any)?.payment?.txId) as any}
|
||||
/>
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -39,13 +39,18 @@ interface ProductFormProps {
|
||||
interface ProductObj {
|
||||
title?: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
price: number; // base QORT price
|
||||
images: string[];
|
||||
category?: string;
|
||||
priceARRR?: number;
|
||||
// Optional per-coin prices (e.g., priceARRR, priceBTC, priceLTC, priceDOGE, priceDGB, priceRVN)
|
||||
priceQORT?: number;
|
||||
priceARRR?: number;
|
||||
priceBTC?: number;
|
||||
priceLTC?: number;
|
||||
priceDOGE?: number;
|
||||
priceDGB?: number;
|
||||
priceRVN?: number;
|
||||
}
|
||||
|
||||
export const ProductForm: React.FC<ProductFormProps> = ({
|
||||
onClose,
|
||||
onSubmit,
|
||||
@@ -94,9 +99,10 @@ export const ProductForm: React.FC<ProductFormProps> = ({
|
||||
const price = Number(value);
|
||||
setProduct({ ...product, price: price });
|
||||
};
|
||||
const handleProductPriceChangeForeign = (value: string, coin:string) => {
|
||||
const handleProductPriceChangeForeign = (value: string, coin: string) => {
|
||||
const price = Number(value);
|
||||
setProduct({ ...product, [`price${coin}`]: price });
|
||||
const key = `price${coin}` as keyof ProductObj;
|
||||
setProduct({ ...product, [key]: price });
|
||||
};
|
||||
|
||||
const handleSelectChange = (event: SelectChangeEvent<string | null>) => {
|
||||
@@ -136,14 +142,12 @@ export const ProductForm: React.FC<ProductFormProps> = ({
|
||||
value: product.price,
|
||||
})
|
||||
|
||||
for (const value of Object.values(CoinFilter)) {
|
||||
if(product[`price${value}`]){
|
||||
price.push(
|
||||
{
|
||||
currency: value,
|
||||
value: +(product[`price${value}`] || 0),
|
||||
},
|
||||
)
|
||||
for (const value of Object.values(CoinFilter) as string[]) {
|
||||
const key = `price${value}` as keyof ProductObj;
|
||||
const maybe = product[key] as unknown;
|
||||
const num = typeof maybe === 'number' ? maybe : Number(maybe as any);
|
||||
if (!Number.isNaN(num) && num > 0) {
|
||||
price.push({ currency: value, value: +num });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ import { CustomInputField } from "../../../components/modals/CreateStoreModal-st
|
||||
import { CustomMenuItem } from "../NewProduct/NewProduct-styles";
|
||||
import { CoinFilter } from "../../Store/Store/Store";
|
||||
import { ARRRSVG } from "../../../assets/svgs/ARRRSVG";
|
||||
import PaymentProof from "../../../components/common/PaymentProof";
|
||||
|
||||
/* <QortalSVG /> must be replaced by <ARRRSVG /> everywhere here depending on the payment info */
|
||||
|
||||
@@ -88,6 +89,8 @@ export const ShowOrder: FC<ShowOrderProps> = ({
|
||||
|
||||
const [statusLoader, setStatusLoader] = useState(false);
|
||||
|
||||
const toTicker = (c: any) => (c === CoinFilter.qort ? "QORT" : c === CoinFilter.arrr ? "ARRR" : String(c).toUpperCase());
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
@@ -490,9 +493,15 @@ export const ShowOrder: FC<ShowOrderProps> = ({
|
||||
<span style={{ fontWeight: 300 }}>
|
||||
{order?.payment?.arrrAddressUsed}
|
||||
</span>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<PaymentProof coin={toTicker(coinToUse) as any} proof={order?.payment?.txId as any} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<CloseDetailsCardIcon
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<PaymentProof coin={"QORT" as any} proof={order?.payment?.transactionSignature as any} />
|
||||
</div>
|
||||
<CloseDetailsCardIcon
|
||||
width={"18"}
|
||||
height={"18"}
|
||||
color={theme.palette.text.primary}
|
||||
@@ -514,6 +523,9 @@ export const ShowOrder: FC<ShowOrderProps> = ({
|
||||
</>
|
||||
);
|
||||
})}
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<PaymentProof coin={"QORT" as any} proof={order?.payment?.transactionSignature as any} />
|
||||
</div>
|
||||
<CloseDetailsCardIcon
|
||||
width={"18"}
|
||||
height={"18"}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { Card, CardContent, CardMedia, useTheme } from "@mui/material";
|
||||
import { CardMedia, useTheme } from "@mui/material";
|
||||
import { RootState } from "../../../state/store";
|
||||
import { Product } from "../../../state/features/storeSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
@@ -22,10 +22,10 @@ import { CoinFilter } from "../Store/Store";
|
||||
function addEllipsis(str: string, limit: number) {
|
||||
if (str.length > limit) {
|
||||
return str.substring(0, limit - 3) + "...";
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
exchangeRate: number | null;
|
||||
@@ -38,19 +38,11 @@ export const ProductCard: React.FC<ProductCardProps> = ({ product, exchangeRate,
|
||||
const theme = useTheme();
|
||||
|
||||
const storeId = useSelector((state: RootState) => state.store.storeId);
|
||||
|
||||
const storeOwner = useSelector((state: RootState) => state.store.storeOwner);
|
||||
|
||||
const user = useSelector((state: RootState) => state.auth.user);
|
||||
const catalogueHashMap = useSelector((state: RootState) => state.global.catalogueHashMap);
|
||||
|
||||
const catalogueHashMap = useSelector(
|
||||
(state: RootState) => state.global.catalogueHashMap
|
||||
);
|
||||
|
||||
const userName = useMemo(() => {
|
||||
if (!user?.name) return "";
|
||||
return user.name;
|
||||
}, [user]);
|
||||
const userName = useMemo(() => user?.name || "", [user]);
|
||||
|
||||
const profileImg = product?.images?.[0];
|
||||
|
||||
@@ -65,19 +57,26 @@ export const ProductCard: React.FC<ProductCardProps> = ({ product, exchangeRate,
|
||||
return;
|
||||
}
|
||||
navigate(
|
||||
`/${
|
||||
product?.user || catalogueHashMap[product?.catalogueId]?.user
|
||||
}/${storeId}/${product?.id}/${product.catalogueId}`
|
||||
`/${product?.user || catalogueHashMap[product?.catalogueId]?.user}/${storeId}/${product?.id}/${product.catalogueId}`
|
||||
);
|
||||
};
|
||||
|
||||
let price = product?.price?.find(item => item?.currency === "qort")?.value;
|
||||
const priceArrr = product?.price?.find(item => item?.currency === CoinFilter.arrr)?.value;
|
||||
if(filterCoin === CoinFilter.arrr && priceArrr) {
|
||||
price = +priceArrr
|
||||
const priceQort = product?.price?.find(item => item?.currency === "qort")?.value;
|
||||
const priceForeign = product?.price?.find(item => item?.currency === filterCoin)?.value;
|
||||
let price = priceQort;
|
||||
if (filterCoin !== CoinFilter.qort && priceForeign) {
|
||||
price = +priceForeign;
|
||||
} else if (priceQort && exchangeRate && filterCoin !== CoinFilter.qort) {
|
||||
price = +priceQort * exchangeRate;
|
||||
}
|
||||
else if(price && exchangeRate && filterCoin !== CoinFilter.qort){
|
||||
price = +price * exchangeRate
|
||||
|
||||
let qortApprox: number | undefined;
|
||||
if (filterCoin !== CoinFilter.qort) {
|
||||
if (priceForeign && exchangeRate) {
|
||||
qortApprox = Number((+priceForeign / exchangeRate).toFixed(8));
|
||||
} else if (priceQort) {
|
||||
qortApprox = Number((+priceQort).toFixed(8));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -112,33 +111,34 @@ export const ProductCard: React.FC<ProductCardProps> = ({ product, exchangeRate,
|
||||
>
|
||||
{filterCoin === CoinFilter.qort && (
|
||||
<AcceptedCoinRow>
|
||||
<QortalSVG
|
||||
color={theme.palette.text.primary}
|
||||
height={"23"}
|
||||
width={"23"}
|
||||
/>{" "}
|
||||
{price}
|
||||
</AcceptedCoinRow>
|
||||
<QortalSVG
|
||||
color={theme.palette.text.primary}
|
||||
height={"23"}
|
||||
width={"23"}
|
||||
/>{" "}
|
||||
{price}
|
||||
</AcceptedCoinRow>
|
||||
)}
|
||||
{filterCoin === CoinFilter.arrr && (
|
||||
<AcceptedCoinRow>
|
||||
<ARRRSVG
|
||||
color={theme.palette.text.primary}
|
||||
height={"23"}
|
||||
width={"23"}
|
||||
/>{" "}
|
||||
{price}
|
||||
</AcceptedCoinRow>
|
||||
{filterCoin !== CoinFilter.qort && (
|
||||
<AcceptedCoinRow>
|
||||
{filterCoin === CoinFilter.arrr ? (
|
||||
<ARRRSVG color={theme.palette.text.primary} height={"23"} width={"23"} />
|
||||
) : (
|
||||
<span style={{ fontWeight: 600 }}>{filterCoin}</span>
|
||||
)}
|
||||
{" "}{price}
|
||||
{qortApprox ? (
|
||||
<div style={{ fontSize: 12, opacity: 0.7, marginTop: 2 }}>≈ {qortApprox} QORT</div>
|
||||
) : null}
|
||||
</AcceptedCoinRow>
|
||||
)}
|
||||
|
||||
</ProductDescription>
|
||||
</StyledCardContent>
|
||||
<div style={{ height: "37px" }}>
|
||||
{storeOwner !== userName && (
|
||||
<AddToCartButton
|
||||
style={{
|
||||
cursor:
|
||||
product.status === "AVAILABLE" ? "pointer" : "not-allowed",
|
||||
cursor: product.status === "AVAILABLE" ? "pointer" : "not-allowed",
|
||||
}}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
@@ -163,11 +163,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({ product, exchangeRate,
|
||||
>
|
||||
{product.status === "AVAILABLE" ? (
|
||||
<>
|
||||
<CartSVG
|
||||
color={theme.palette.text.primary}
|
||||
height={"18"}
|
||||
width={"18"}
|
||||
/>{" "}
|
||||
<CartSVG color={theme.palette.text.primary} height={"18"} width={"18"} />{" "}
|
||||
Add to Cart
|
||||
</>
|
||||
) : product.status === "OUT_OF_STOCK" ? (
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
Rating,
|
||||
Typography,
|
||||
Skeleton,
|
||||
Select,
|
||||
MenuItem,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
Catalogue,
|
||||
@@ -79,6 +81,7 @@ import { ExpandMoreSVG } from "../../../assets/svgs/ExpandMoreSVG";
|
||||
import { StoreDetails } from "../StoreDetails/StoreDetails";
|
||||
import { StoreReviews } from "../StoreReviews/StoreReviews";
|
||||
import { setNotification } from "../../../state/features/notificationsSlice";
|
||||
import { getPriceHint } from "../../../lib/pricing";
|
||||
import {
|
||||
DATA_CONTAINER_BASE,
|
||||
REVIEW_BASE,
|
||||
@@ -86,6 +89,7 @@ import {
|
||||
} from "../../../constants/identifiers";
|
||||
import QORT from "../../../assets/img/qort.png";
|
||||
import ARRR from "../../../assets/img/arrr.png";
|
||||
import { coinPng } from "../../../constants/coin-icons";
|
||||
import {
|
||||
AcceptedCoin,
|
||||
ExchangeRateCard,
|
||||
@@ -114,6 +118,11 @@ enum DateFilter {
|
||||
export enum CoinFilter {
|
||||
qort = "QORT",
|
||||
arrr = "ARRR",
|
||||
btc = "BTC",
|
||||
ltc = "LTC",
|
||||
doge = "DOGE",
|
||||
dgb = "DGB",
|
||||
rvn = "RVN",
|
||||
}
|
||||
|
||||
export const Store = () => {
|
||||
@@ -189,35 +198,26 @@ export const Store = () => {
|
||||
: currentViewedStore
|
||||
}, [username, user?.name, currentStore, currentViewedStore])
|
||||
|
||||
const calculateARRRExchangeRate = async()=> {
|
||||
const calculateExchangeRate = async (coin: string) => {
|
||||
try {
|
||||
const url = '/crosschain/price/PIRATECHAIN?maxtrades=10&inverse=true'
|
||||
const info = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseDataStore = await info.text();
|
||||
|
||||
const ratio = +responseDataStore /100000000
|
||||
if(isNaN(ratio)) throw new Error('Cannot get exchange rate')
|
||||
setExchangeRate(ratio)
|
||||
} catch (error) {
|
||||
dispatch(setPreferredCoin(CoinFilter.qort))
|
||||
dispatch(
|
||||
setNotification({
|
||||
alertType: "error",
|
||||
msg: "Cannot get exchange rate- reverted to QORT",
|
||||
})
|
||||
);
|
||||
const ratio = await getPriceHint(coin);
|
||||
if (!ratio) {
|
||||
dispatch(setPreferredCoin(CoinFilter.qort));
|
||||
dispatch(setNotification({ alertType: "warning", msg: "Cannot get exchange rate- reverted to QORT" }));
|
||||
setExchangeRate(null);
|
||||
return;
|
||||
}
|
||||
setExchangeRate(ratio);
|
||||
} catch (e) {
|
||||
dispatch(setPreferredCoin(CoinFilter.qort));
|
||||
dispatch(setNotification({ alertType: "warning", msg: "Cannot get exchange rate- reverted to QORT" }));
|
||||
setExchangeRate(null);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const switchCoin = async ()=> {
|
||||
const switchCoin = async ()=> {
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
await calculateARRRExchangeRate()
|
||||
await calculateExchangeRate(preferredCoin as string);
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
}
|
||||
|
||||
@@ -229,14 +229,16 @@ export const Store = () => {
|
||||
}, [userOwnDataContainer]);
|
||||
|
||||
useEffect(()=> {
|
||||
if(preferredCoin === CoinFilter.arrr && storeToUse?.supportedCoins?.includes(CoinFilter.arrr)){
|
||||
if(preferredCoin !== CoinFilter.qort && storeToUse?.supportedCoins?.includes(preferredCoin)){
|
||||
switchCoin()
|
||||
}
|
||||
} else {
|
||||
setExchangeRate(null)
|
||||
}
|
||||
}, [preferredCoin, storeToUse])
|
||||
|
||||
const coinToUse = useMemo(()=> {
|
||||
if(preferredCoin === CoinFilter.arrr && storeToUse?.supportedCoins?.includes(CoinFilter.arrr)){
|
||||
return CoinFilter.arrr
|
||||
if(storeToUse?.supportedCoins?.includes(preferredCoin)){
|
||||
return preferredCoin
|
||||
} else {
|
||||
return CoinFilter.qort
|
||||
}
|
||||
@@ -638,7 +640,7 @@ export const Store = () => {
|
||||
setCategoryChips(prevChips => prevChips.filter(c => c !== chip));
|
||||
};
|
||||
|
||||
if (isLoadingGlobal) return;
|
||||
if (isLoadingGlobal) return null;
|
||||
|
||||
if (!currentViewedStore && username !== user?.name && !isLoadingGlobal)
|
||||
return (
|
||||
@@ -766,43 +768,56 @@ export const Store = () => {
|
||||
/>
|
||||
</FiltersTitle>
|
||||
<FiltersSubContainer>
|
||||
<FiltersRow>
|
||||
<AcceptedCoinRow>
|
||||
QORT
|
||||
<AcceptedCoin src={QORT} alt="QORT-logo" />
|
||||
</AcceptedCoinRow>
|
||||
<FiltersCheckbox
|
||||
checked={coinToUse === CoinFilter.qort}
|
||||
onChange={() => {
|
||||
|
||||
if (coinToUse !== CoinFilter.qort) {
|
||||
dispatch(setPreferredCoin(CoinFilter.qort))
|
||||
|
||||
}
|
||||
|
||||
|
||||
{/* Prices In single-select */}
|
||||
<FormControl fullWidth size="small" sx={{ mt: 1 }}>
|
||||
<Select
|
||||
value={String(coinToUse).toUpperCase()}
|
||||
onChange={(e) => {
|
||||
const sym = String(e.target.value);
|
||||
const cf = (CoinFilter as any)[sym.toLowerCase()];
|
||||
if (cf) dispatch(setPreferredCoin(cf));
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
{storeToUse?.foreignCoins?.ARRR && storeToUse?.supportedCoins?.includes('ARRR') && (
|
||||
<FiltersRow>
|
||||
<AcceptedCoinRow>
|
||||
ARRR
|
||||
<AcceptedCoin src={ARRR} alt="ARRR-logo" />
|
||||
</AcceptedCoinRow>
|
||||
<FiltersCheckbox
|
||||
checked={coinToUse === CoinFilter.arrr}
|
||||
onChange={() => {
|
||||
if (coinToUse !== CoinFilter.arrr) {
|
||||
dispatch(setPreferredCoin(CoinFilter.arrr))
|
||||
}
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
>
|
||||
{[
|
||||
"QORT",
|
||||
...(storeToUse?.supportedCoins || []).filter(Boolean)
|
||||
].filter((v, i, a) => a.indexOf(v) === i).map((sym) => (
|
||||
<MenuItem key={sym} value={sym}>
|
||||
<span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
|
||||
<img src={coinPng(sym) || ""} alt={`${sym}-logo`} width={22} height={22} />
|
||||
{sym}
|
||||
</span>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{coinToUse !== CoinFilter.qort && exchangeRate && (
|
||||
<ExchangeRateCard>
|
||||
<ExchangeRateRow>
|
||||
<ExchangeRateTitle>1 QORT = {exchangeRate} {coinToUse}</ExchangeRateTitle>
|
||||
</ExchangeRateRow>
|
||||
<ExchangeRateRow>
|
||||
<ExchangeRateTitle>
|
||||
1 {String(coinToUse)} = {Number((1 / (exchangeRate || 1)).toFixed(8))} QORT
|
||||
</ExchangeRateTitle>
|
||||
</ExchangeRateRow>
|
||||
<ExchangeRateRow>
|
||||
<ExchangeRateSubTitle>
|
||||
{`Rate calculated by recent trade portal trades`}
|
||||
</ExchangeRateSubTitle>
|
||||
</ExchangeRateRow>
|
||||
<ExchangeRateRow style={{ gap: "10px" }}>
|
||||
<AcceptedCoin src={coinPng('QORT') || ''} alt="QORT-logo" />
|
||||
<CompareArrowsSVG
|
||||
color={theme.palette.text.primary}
|
||||
height={"32"}
|
||||
width={"32"}
|
||||
/>
|
||||
<AcceptedCoin src={coinPng(String(coinToUse)) || ''} alt={`${coinToUse}-logo`} />
|
||||
</ExchangeRateRow>
|
||||
</ExchangeRateCard>
|
||||
)}
|
||||
|
||||
</FiltersSubContainer>
|
||||
<FiltersTitle>
|
||||
Date Product Added
|
||||
@@ -836,30 +851,6 @@ export const Store = () => {
|
||||
/>
|
||||
</FiltersRow>
|
||||
</FiltersSubContainer>
|
||||
{coinToUse === CoinFilter.arrr && exchangeRate && (
|
||||
<FiltersSubContainer>
|
||||
<ExchangeRateCard>
|
||||
<ExchangeRateRow>
|
||||
<ExchangeRateTitle>1 QORT = {exchangeRate} ARRR</ExchangeRateTitle>
|
||||
</ExchangeRateRow>
|
||||
<ExchangeRateRow>
|
||||
<ExchangeRateSubTitle>
|
||||
{`Rate calculated by recent trade portal trades`}
|
||||
</ExchangeRateSubTitle>
|
||||
|
||||
</ExchangeRateRow>
|
||||
<ExchangeRateRow style={{ gap: "10px" }}>
|
||||
<AcceptedCoin src={QORT} alt="QORT-logo" />
|
||||
<CompareArrowsSVG
|
||||
color={theme.palette.text.primary}
|
||||
height={"32"}
|
||||
width={"32"}
|
||||
/>
|
||||
<AcceptedCoin src={ARRR} alt="ARRR-logo" />
|
||||
</ExchangeRateRow>
|
||||
</ExchangeRateCard>
|
||||
</FiltersSubContainer>
|
||||
)}
|
||||
|
||||
</FiltersContainer>
|
||||
</FiltersCol>
|
||||
@@ -924,7 +915,18 @@ export const Store = () => {
|
||||
)}
|
||||
|
||||
</AcceptedCoinRow>
|
||||
</OfferedCoinsRow>
|
||||
{/* other coin icons */}
|
||||
{storeToUse?.foreignCoins && storeToUse?.supportedCoins && Object.keys(storeToUse.foreignCoins)
|
||||
.filter(sym => sym !== 'QORT' && sym !== 'ARRR' && storeToUse.supportedCoins?.includes(sym as any))
|
||||
.map((sym) => (
|
||||
<AcceptedCoin
|
||||
key={sym}
|
||||
style={{ width: "26px", height: "26px" }}
|
||||
src={coinPng(sym) || ""}
|
||||
alt={`${sym}-logo`}
|
||||
/>
|
||||
))}
|
||||
</OfferedCoinsRow>
|
||||
</StoreTitleCol>
|
||||
</StoreTitleCard>
|
||||
{username === user?.name ? (
|
||||
|
||||
@@ -28,6 +28,7 @@ import { RootState } from "../../../state/store";
|
||||
import { clearViewedStoreDataContainer } from "../../../state/features/storeSlice";
|
||||
import QORT from "../../../assets/img/qort.png";
|
||||
import ARRR from "../../../assets/img/arrr.png";
|
||||
import { coinPng } from "../../../constants/coin-icons";
|
||||
|
||||
interface StoreCardProps {
|
||||
storeTitle: string;
|
||||
@@ -134,10 +135,9 @@ export const StoreCard: FC<StoreCardProps> = ({
|
||||
)}
|
||||
</StoreCardInfo>
|
||||
<AcceptedCoinsRow>
|
||||
<AcceptedCoin src={QORT} alt="QORT-logo" />
|
||||
{supportedCoins?.includes('ARRR') && (
|
||||
<AcceptedCoin src={ARRR} alt="ARRR-logo" />
|
||||
)}
|
||||
{(supportedCoins || []).map((sym) => (
|
||||
<AcceptedCoin key={sym} src={coinPng(sym) || ""} alt={`${sym}-logo`} />
|
||||
))}
|
||||
</AcceptedCoinsRow>
|
||||
<StoreCardOwner>{storeOwner}</StoreCardOwner>
|
||||
{storeOwner === userName && (
|
||||
|
||||
@@ -41,8 +41,10 @@ interface Delivery {
|
||||
interface Payment {
|
||||
total: number
|
||||
currency: string
|
||||
transactionSignature: string
|
||||
arrrAddressUsed?:string
|
||||
transactionSignature?: string
|
||||
arrrAddressUsed?: string
|
||||
addressUsed?: string
|
||||
txId?: string
|
||||
}
|
||||
enum CommunicationMethod {
|
||||
QMail = 'Q-Mail'
|
||||
|
||||
15
src/types/shims.d.ts
vendored
Normal file
15
src/types/shims.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
// Ambient shim types to unblock build in Cart and elsewhere until dedicated patches land.
|
||||
declare global {
|
||||
// Some paths reference a global `responseSendCoin`; provide a lenient shape.
|
||||
const responseSendCoin: {
|
||||
txId?: string;
|
||||
transactionId?: string;
|
||||
signature?: string;
|
||||
data?: { txId?: string; signature?: string };
|
||||
};
|
||||
|
||||
// Some codebases have untyped qortalRequest; keep it permissive.
|
||||
function qortalRequest(body: any): Promise<any>;
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -6,9 +6,7 @@ import { RootState } from "../state/store";
|
||||
import CreateStoreModal, {
|
||||
onPublishParam,
|
||||
} from "../components/modals/CreateStoreModal";
|
||||
import EditStoreModal, {
|
||||
onPublishParamEdit,
|
||||
} from "../components/modals/EditStoreModal";
|
||||
import EditStoreModal from "../components/modals/EditStoreModal";
|
||||
import {
|
||||
setCurrentStore,
|
||||
setDataContainer,
|
||||
@@ -109,6 +107,7 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
|
||||
const [userAvatar, setUserAvatar] = useState<string>("");
|
||||
const [closeCreateStoreModal, setCloseCreateStoreModal] =
|
||||
useState<boolean>(false);
|
||||
const [closeEditStoreModal, setCloseEditStoreModal] = useState<boolean>(false);
|
||||
const [hasAttemptedToFetchShopInitial, setHasAttemptedToFetchShopInitial] =
|
||||
useState<boolean>(false);
|
||||
const [storedDataContainer, setStoredDataContainer] =
|
||||
@@ -447,15 +446,9 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
|
||||
);
|
||||
|
||||
const editStore = React.useCallback(
|
||||
async ({
|
||||
title,
|
||||
description,
|
||||
location,
|
||||
shipsTo,
|
||||
logo,
|
||||
foreignCoins,
|
||||
supportedCoins,
|
||||
}: onPublishParamEdit) => {
|
||||
async (param: any) => {
|
||||
const { title, description, location, shipsTo, logo, foreignCoins, supportedCoins } = param as any;
|
||||
|
||||
if (!user || (!user.selectedName && !user.name) || !currentStore)
|
||||
throw new Error("Cannot publish: You do not have a Qortal name");
|
||||
if (!title) throw new Error("A title is required");
|
||||
@@ -806,8 +799,10 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
|
||||
)}
|
||||
<EditStoreModal
|
||||
open={isOpenEditStoreModal}
|
||||
onClose={onCloseEditStoreModal}
|
||||
onPublish={editStore}
|
||||
onUpdate={editStore}
|
||||
closeEditStoreModal={closeEditStoreModal}
|
||||
setCloseEditStoreModal={setCloseEditStoreModal}
|
||||
store={currentStore as any}
|
||||
username={user?.selectedName || user?.name || ""}
|
||||
/>
|
||||
{/* Trigger reusable modal if something goes wrong during creation of the datacontainer */}
|
||||
@@ -876,7 +871,7 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
|
||||
<DownloadNowButton
|
||||
onClick={() => {
|
||||
const userOS = parser.getOS().name;
|
||||
if (userOS?.includes("Android" || "iOS")) {
|
||||
if (userOS?.includes("Android") || userOS?.includes("iOS")) {
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: "Qortal is not available on mobile devices yet. Please download on a desktop or laptop.",
|
||||
|
||||
Reference in New Issue
Block a user