Enhance documentation, implement Twilio OTP delivery, and update payment gateway methods. Updated AGENTS.md and README.md for clarity on ExecPlans and architecture. Added Twilio as a dependency and implemented capture/refund methods in MoyasarGateway. Improved frontend routing with react-router-dom and added authentication context. Updated styles and localization files for better user experience.

This commit is contained in:
2026-02-28 15:33:50 +03:00
parent 86fd07c778
commit a1da918f95
37 changed files with 1645 additions and 277 deletions
+123
View File
@@ -0,0 +1,123 @@
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { apiPost } from "../api/client";
function generateIdempotencyKey() {
if (typeof crypto !== "undefined" && crypto.randomUUID) {
return crypto.randomUUID();
}
return `id-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
// Must match AuthContext's STORAGE_ACCESS so fallback finds the same token
const AUTH_TOKEN_KEY = "auth_access";
export function usePaymentForm(bookingId = "", token = "") {
// token: optional auth token from AuthContext; tokenInput: manual override from form
const { t } = useTranslation();
const [bookingIdInput, setBookingIdInput] = useState(bookingId);
const [tokenInput, setTokenInput] = useState(() => {
if (token) return token;
if (typeof window !== "undefined") {
return localStorage.getItem(AUTH_TOKEN_KEY) || "";
}
return "";
});
const [sourceType, setSourceType] = useState("stcpay");
const [sourceValue, setSourceValue] = useState("");
const [callbackUrl, setCallbackUrl] = useState(() => {
if (typeof window !== "undefined") {
return `${window.location.origin}/pay/return`;
}
return "";
});
const [status, setStatus] = useState("idle");
const [result, setResult] = useState(null);
const [error, setError] = useState("");
const idempotencyKey = useMemo(generateIdempotencyKey, []);
// Persist token to localStorage when it changes
const setTokenInputAndPersist = (value) => {
setTokenInput(value);
if (typeof window !== "undefined") {
if (value) {
localStorage.setItem(AUTH_TOKEN_KEY, value);
} else {
localStorage.removeItem(AUTH_TOKEN_KEY);
}
}
};
async function submit() {
setStatus("loading");
setError("");
setResult(null);
if (!bookingIdInput) {
setStatus("error");
setError(t("payment.errors.bookingRequired"));
return;
}
const source = { type: sourceType };
if (sourceType === "stcpay") {
if (!sourceValue) {
setStatus("error");
setError(t("payment.errors.mobileRequired"));
return;
}
source.mobile = sourceValue;
}
if (sourceType === "token") {
if (!sourceValue) {
setStatus("error");
setError(t("payment.errors.tokenRequired"));
return;
}
source.token = sourceValue;
}
const payload = {
booking_id: Number(bookingIdInput),
provider: "moyasar",
idempotency_key: idempotencyKey,
source,
};
if (callbackUrl) {
payload.callback_url = callbackUrl;
}
// Use tokenInput (form state) so user edits are respected; token prop only initializes it
const authToken = tokenInput;
try {
const data = await apiPost("/payments/", payload, authToken || undefined);
setResult(data);
setStatus("ready");
if (data?.redirect_url) {
window.location.assign(data.redirect_url);
}
} catch (err) {
setStatus("error");
setError(err.message || t("payment.errors.generic"));
}
}
return {
bookingIdInput,
setBookingIdInput,
tokenInput,
setTokenInput: setTokenInputAndPersist,
sourceType,
setSourceType,
sourceValue,
setSourceValue,
callbackUrl,
setCallbackUrl,
idempotencyKey,
status,
result,
error,
submit,
};
}
+33
View File
@@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
import { apiGet } from "../api/client";
export function useSalonSearch(query) {
const [salons, setSalons] = useState([]);
const [status, setStatus] = useState("idle");
useEffect(() => {
let ignore = false;
async function load() {
setStatus("loading");
try {
const data = await apiGet(`/salons/?q=${encodeURIComponent(query)}`);
if (!ignore) {
setSalons(data);
setStatus("ready");
}
} catch {
if (!ignore) {
setStatus("error");
}
}
}
load();
return () => {
ignore = true;
};
}, [query]);
return { salons, status };
}