How to exercise the bridge without a payment gateway, how to get your page onto a real device, and the five things to check first when a call does not come back.
Studio's API Mock can answer callNativeAPI from your page, so you can build against orderPayment long before a real gateway exists. The field that matters is mockType:
[
{
"name": "orderPayment from the web page",
"apiName": "orderPayment",
"mockType": "H5",
"returnType": "Success",
"returnValue": "{\"orderId\":\"mock-1\",\"status\":\"paid\",\"totalAmount\":\"126.50\"}",
"enable": true,
"validate": true,
"auto": false
}
]mockType must be H5 for a call made from a web page. A rule of type API covers mini-app JavaScript and never reaches your page, so the call fails with api not implemented and the mock looks broken when it simply does not apply. Add both types if the same function is called from both sides.A .custom_api_handlers/ directory has the same limitation in stronger form: it serves mini-app JavaScript only and cannot answer a page at all. Use an API Mock rule.
The most useful thing you can build early is honest error reporting. The host's response shape varies — sometimes a flat errMsg, sometimes an envelope wrapping a different inner message, sometimes a bare string, occasionally null — and code that normalises before judging gets it wrong in a specific, expensive way.
// Print what the host said before anything you concluded from it.
//
// Judge the RAW response, then unwrap — never the other way round. Unwrapping to res.data
// first discards the envelope's own errMsg, and the inner message often contains no "fail",
// so a declined call reads as a success.
ft.callNativeAPI("orderPayment", payload, function (res) {
console.log("[page] orderPayment RAW:", res);
var msg = String(deepFind(res, "errMsg") || (typeof res === "string" ? res : ""));
var code = deepFind(res, "errCode");
if (/fail|error|deny/i.test(msg)) return onFailure(res);
// A non-zero errCode is a failure even when no message says so.
if (typeof code === "number" && code !== 0) return onFailure(res);
// The callback fired with nothing. That is not a successful payment.
if (res === null || res === undefined) return onFailure(res);
onSuccess(res);
});
// The host does not always put errMsg at the top level, and a check that only looks one
// level deep reports "no errMsg" when there is one.
function deepFind(o, key, depth) {
depth = depth || 0;
if (!o || typeof o !== "object" || depth > 6) return undefined;
if (o[key] !== undefined) return o[key];
for (var k in o) {
if (!Object.prototype.hasOwnProperty.call(o, k)) continue;
var hit = deepFind(o[k], key, depth + 1);
if (hit !== undefined) return hit;
}
return undefined;
}res.data first throws away the envelope's own errMsg, and the inner message frequently contains no "fail" — so a declined payment reads as a success. Keep the whole response and print it above whatever you concluded from it.The simulator will take localhost with URL checking off. A device will not: it cannot reach your laptop, and localhost can never be allow-listed. So you need real HTTPS at the top level — an HTTPS tunnel to your dev server is the usual answer, and the tunnel's host must go in the portal's allow list like any other domain.
Serving your page from anything that wraps it in an app shell will not work, however valid the HTTPS — see the frame restriction in overview & SDK.
In this order:
A relayed call has four places to fail and one symptom. Log both legs in the shell — what arrived and what was sent back, with the payload and not just success or failure. "Fail" on its own sends you to inspect the relay when the relay worked perfectly and the API behind it refused.
On a device there is no console, so a toast on each leg is the only way to see it. If the inbound toast never appears, the request never arrived and the reply is irrelevant; if it appears and the page still times out, the problem is the reply.