Thought for 18s
Here’s the clean way to make your React booking form feel native inside your CS-Cart storefront (same domain, same header/footer, no “different site” vibes), while still using storefront_rest_api and graphql_api safely.
Target architecture (no visible “second website”)
- Serve the React app from CS-Cart at a URL like
/booking (it loads inside your normal theme wrapper).
- Call CS-Cart APIs via a same-origin proxy endpoint you add (PHP controller in an add-on).
- React calls:
/booking/api/rest/... and /booking/api/graphql
- The proxy calls internal CS-Cart endpoints:
- Keep credentials server-side (don’t ever put API keys in React).
1) CS-Cart add-on: mount your React app at /booking
app/addons/booking_react/addon.xml
<?xml version="1.0"?>
<addon scheme="3.0">
<id>booking_react</id>
<version>1.0</version>
<priority>1000</priority>
<default_language>en</default_language>
<name>Booking React</name>
<description>Serves React booking app and proxies Storefront REST + GraphQL</description>
</addon>
app/addons/booking_react/init.php
<?php
if (!defined('BOOTSTRAP')) { die('Access denied'); }
fn_register_hooks();
app/addons/booking_react/controllers/frontend/booking.php
<?php
if (!defined('BOOTSTRAP')) { die('Access denied'); }
if ($mode === 'index') {
// renders design/themes/<your_theme>/templates/addons/booking_react/views/booking/index.tpl
Tygh::$app['view']->assign('react_asset_base', 'js/addons/booking_react'); // where you’ll deploy built assets
}
return [CONTROLLER_STATUS_OK];
design/themes/<your_theme>/templates/addons/booking_react/views/booking/index.tpl
{extends file="views/index/index.tpl"}
{block name="content"}
<div id="booking-app"></div>
{* Example: Vite build outputs booking-app.js + booking-app.css *}
<link rel="stylesheet" href="{$config.current_location}/design/themes/{$runtime.layout.theme_name}/media/{$react_asset_base}/booking-app.css">
<script defer src="{$config.current_location}/design/themes/{$runtime.layout.theme_name}/media/{$react_asset_base}/booking-app.js"></script>
{/block}
Result: /booking is a normal storefront page (same domain/theme), just with your React app mounted.
2) Add a safe proxy for Storefront REST API + GraphQL
Why: CS-Cart REST uses auth (commonly Basic auth with API key) docs.cs-cart.com — you don’t want that exposed to the browser.
app/addons/booking_react/controllers/frontend/booking_api.php
This exposes:
GET/POST /booking_api.rest?path=sra_products... → forwards to /api/4.0/<path>
POST /booking_api.graphql → forwards to /graphql
<?php
use Tygh\Http;
use Tygh\Registry;
if (!defined('BOOTSTRAP')) { die('Access denied'); }
$base_url = Registry::get('config.current_location');
// Store these in add-on settings in real life (fn_get_addon_option)
$api_user = Registry::get('addons.booking_react.api_user'); // e.g. an API user email
$api_key = Registry::get('addons.booking_react.api_key'); // API key
function booking_react_basic_auth_header($user, $key) {
return 'Basic ' . base64_encode($user . ':' . $key);
}
if ($mode === 'rest') {
$path = isset($_REQUEST['path']) ? ltrim((string) $_REQUEST['path'], '/') : '';
if ($path === '') {
return [CONTROLLER_STATUS_NO_PAGE];
}
// Storefront REST API add-on endpoints are under /api/4.0/<entity> :contentReference[oaicite:3]{index=3}
$url = $base_url . '/api/4.0/' . $path;
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$headers = [
'Authorization: ' . booking_react_basic_auth_header($api_user, $api_key),
'Content-Type: application/json',
'Accept: application/json',
];
// Forward query string except our routing param
$query = $_GET;
unset($query['dispatch'], $query['path']);
if (!empty($query)) {
$url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($query);
}
$body = null;
if ($method !== 'GET') {
$raw = file_get_contents('php://input');
$body = $raw !== false ? $raw : null;
}
$resp = Http::request($method, $url, $body, [
'headers' => $headers,
'timeout' => 20,
]);
// Return raw JSON
header('Content-Type: application/json; charset=utf-8');
echo $resp;
exit;
}
if ($mode === 'graphql') {
$url = $base_url . '/graphql';
$raw = file_get_contents('php://input');
if ($raw === false || $raw === '') {
$raw = json_encode(['query' => '{ __typename }']);
}
$headers = [
'Authorization: ' . booking_react_basic_auth_header($api_user, $api_key),
'Content-Type: application/json',
'Accept: application/json',
];
$resp = Http::post($url, $raw, [
'headers' => $headers,
'timeout' => 20,
]);
header('Content-Type: application/json; charset=utf-8');
echo $resp;
exit;
}
return [CONTROLLER_STATUS_NO_PAGE];
Add-on settings (recommended)
Add a settings section so you can set api_user and api_key in Admin → Add-ons, rather than hardcoding. (If you want, I’ll drop in the schema/settings files.)
3) React: call your proxy (same origin)
src/api/client.ts
type GraphQLResponse<T> = { data?: T; errors?: { message: string }[] };
export async function gql<T>(query: string, variables?: Record<string, any>): Promise<T> {
const r = await fetch("/booking_api.graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
credentials: "include",
});
const json = (await r.json()) as GraphQLResponse<T>;
if (!r.ok || json.errors?.length) throw new Error(json.errors?.[0]?.message || "GraphQL error");
return json.data as T;
}
export async function sraGet<T>(path: string, params?: Record<string, string | number>): Promise<T> {
const u = new URL("/booking_api.rest", window.location.origin);
u.searchParams.set("path", path);
if (params) Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, String(v)));
const r = await fetch(u.toString(), { credentials: "include" });
if (!r.ok) throw new Error(`REST error ${r.status}`);
return (await r.json()) as T;
}
export async function sraPost<T>(path: string, body: any): Promise<T> {
const u = new URL("/booking_api.rest", window.location.origin);
u.searchParams.set("path", path);
const r = await fetch(u.toString(), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
credentials: "include",
});
if (!r.ok) throw new Error(`REST error ${r.status}`);
return (await r.json()) as T;
}
Example usage: fetch products via Storefront REST API add-on
The add-on maps entities like sra_products (example shown by CS-Cart team). CS-Cart Forums
import { sraGet } from "./api/client";
async function loadProducts() {
// Example: /api/4.0/sra_products
const data = await sraGet<any>("sra_products", { page: 1, items_per_page: 20 });
return data;
}
Example usage: GraphQL introspection (so we don’t guess your schema)
import { gql } from "./api/client";
const INTROSPECT = `
query Introspect {
__schema {
mutationType { name }
queryType { name }
types { name kind }
}
}`;
async function checkSchema() {
return await gql<any>(INTROSPECT);
}
4) How booking “submits” into CS-Cart (two solid options)
Option A (recommended): Treat booking as a normal CS-Cart checkout
- Your React form:
- creates/updates cart contents (e.g., a “Booking Deposit” product or the service product)
- attaches booking metadata (notes / custom fields)
- triggers place-order
- Benefit: all taxes/shipping/payments/Stripe Connect etc. behave normally.
Important: I’m not assuming the exact “place order” endpoint name for storefront_rest_api because it varies by build/add-ons; the safest approach is:
- use GraphQL introspection to find the cart/order mutations available, or
- tell me which CS-Cart version + whether your mobile API add-ons are enabled, and I’ll match your exact endpoints.
Option B: Create a “booking request” entity (custom table) then convert to order in admin
- React submits booking request into your add-on table.
- Admin reviews → “Create order” from request (or auto-create).
- Benefit: minimal coupling; drawback: not instant checkout unless you implement conversion flow.
Quick integration notes (so it feels seamless)
- Put
/booking into your storefront menu like any other page.
- Keep all API calls same-origin (
/booking_api...) so there are no CORS headaches.
- If you want customer identity inside React, you can expose a tiny endpoint like
/booking_api.me that returns auth.user_id, email, etc. from CS-Cart session (no API key required).
If you paste these 3 things, I’ll lock it to your exact endpoints (no guessing)
- Your CS-Cart version (e.g. 4.xx)
- Whether
storefront_rest_api and graphql_api add-ons are enabled (and their versions if shown)
- What a “booking” should create: cart+order, or request record, or both
(But you can already implement the mounting + proxy exactly as above.)