Shoppingate Logo
HomePartnerSell on E-commAbout UsContact UsDevelopers
Documentation
  • Getting started
  • Choosing your path
  • Introduction
    • Introduction
    • Sign up & login
    • Dashboard & analytics
    • Mini-Programs
    • Submit & publish a version
    • App Binding
    • Privacy Settings
    • Allowed URL
    • Users & roles
    • Introduction
    • Install & log in
    • Create a project
    • The workspace
    • Preview & upload
  • app.json config
  • Lifecycle
  • Routing & query
  • Host functions
  • orderPayment
  • Permissions
  • User info
  • Deep links
  • Overview & SDK
  • The wrapper
  • What a page can reach
  • Host functions
  • Permissions
  • Look and feel
  • Testing
  • Authentication
  • Endpoints
  • Webhooks
Web view

Testing

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.

Mocking a host call

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.

Print what the host said

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;
}
Judge the raw response, then unwrap. Never the reverse. Unwrapping to 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.

Getting onto a device

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.

When a call does not come back

In this order:

Check
Why
The page is the top-level document
window.top === window.self. In a frame the SDK loads and every call times out instead of erroring.
The host is in the domain allow list
Register it in the developer portal. Without it the web-view loads nothing and binderror is the only hint.
downloadFile has its own allow list
A separate list from the web-view one, so the page can load perfectly and a download still fail.
ft.complete() is called
If the mini-app sets a load timeout, a page that never calls it is treated as failed however well it rendered.
HTTPS, not localhost
A device cannot reach your laptop, and localhost can never be allow-listed. Use a tunnel.

Which leg of the relay failed

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.

Need help? We’re here.

Our team is ready to support you in every step of your experience.

Email Us
[email protected]
Sell With SGContact UsTerms of ServicePrivacy PolicyFAQTravel FAQ
Connect with us
X / TwitterLinkedInInstagramFacebookTiktok

Copyright © Shoppingate 2026

Mada
Visa
Mastercard
Apple Pay
Tamara