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:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user