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

What a page can reach

Some things your page calls itself. Most things only the shell can call, and the page asks it to. Knowing which is which before you plan the work is the difference between a week and a month.

The headline: a wrapped page can take payment. Host functions are reachable from page JavaScript and return their result to a callback. Rebuilding a checkout as a native page is one option, not a requirement.

Direct — your page calls these itself

ft.x in page JavaScript. No shell involvement, nothing to declare, nothing to relay.

What
API
Notes
Location
ft.getLocation
Where the phone is. 'gcj02' for map display, 'wgs84' for raw GPS.
Location on a map
ft.chooseLocation
What the customer PICKED — name, address, lat, lng. The one you want for a delivery address.
Photo
ft.chooseImage
One picker for taking a photo and for choosing an existing one — sourceType decides which. Answers with tempFilePaths (strings) and tempFiles ({ path, size }).
Host functions
ft.callNativeAPI
Payment, order history, support tickets. A round trip — the callback returns the result.
HTTP requests
ft.request
Subject to the same domain allow list as the page itself.
Storage
ft.setStorage / ft.getStorage
Scoped to the mini-app, not to the page's origin.

Host functions, from the page

ft.callNativeAPI(name, data, callback) reaches Shoppingate's own functions — payment, order history, support tickets. The callback carries the result, so this is a round trip rather than a one-way post.

// Host functions are reachable straight from page JavaScript, so a wrapped checkout can
// take payment as it stands. This path goes directly to the host; SGConf.js is not involved.
ft.callNativeAPI("orderPayment", {
  orderInfo: {
    items: [{ sku: "demo-1", name: "Premium dates", qty: 1,
              unit_price: "98.00", total_amount: "98.00" }],
    subtotal:      "98.00",
    delivery_fee:  "12.00",
    tax_amount:    "16.50",
    total_amount: "126.50",   // subtotal + delivery_fee + tax_amount must balance
    currency: "SAR",
    vendor_order_ref: "order-1042",
    order_type: "delivery",
  },
  // Stable per order. The same key on a retry is what stops a double charge.
  idempotencyKey: "order-1042",
  miniappId: "<your mini-app id>",
}, function (res) {
  console.log("payment result", res);
});

Two things that will reject a call before it does anything: the amounts inside orderInfo must balance — subtotal + delivery_fee + tax_amount equals total_amount — and miniappId must be the real id, not a placeholder.

No SGConf.js entry is needed for any of this. That table exists for native pages calling ft.<name>(…); the callNativeAPI path does not read it.

All nine host functions, with their request fields, responses and the web-view call for each, are on host functions — including the result field that decides whether the work actually happened, and where miniappId comes from in a page.

Relay — the shell calls these for you

Everything below is a mini-app runtime API, so the shell calls it and relays the answer to your page. ft.getUserInfo is the one you will reach for first.

What
API the shell calls
Notes
Profile
ft.getUserInfo
The shell calls it and relays the answer. Prompts on every call, so read it once per launch and cache.
Microphone
ft.startRecord
Always pair with stopRecord — a recorder left running outlives the page.
Save to album
ft.saveImageToPhotosAlbum
filePath will not take a URL. Download first with ft.downloadFile, save the temp path.
Bluetooth
ft.openBluetoothAdapter
Close the adapter when you are done; a scan left running drains the battery.
Add a contact
ft.addPhoneContact
Writes a contact into the phone's address book. There is no undo, so confirm before calling it.
Calendar
ft.addPhoneCalendar
startTime is in SECONDS. Milliseconds put the event tens of thousands of years out, silently.

The channel is two one-way pipes

The channel is two independent sends, and you pair them yourself:

page → shellft.miniProgram.sendWebviewEvent({…})→the <web-view>'s bindevent
shell → pageft.sendWebviewEvent({…}, pageInstance)→ft.miniProgram.onWebviewEvent(cb)

A correlation id is what makes them a pair. Without one, a second request landing before the first reply is indistinguishable from the first one answering — and the two promises resolve with each other's results.

In the page
// The page asks the shell to do what the page cannot.
//
// These are two INDEPENDENT one-way pipes:
//   page  -> shell : ft.miniProgram.sendWebviewEvent({...})   → the <web-view>'s bindevent
//   shell -> page  : ft.sendWebviewEvent({...}, pageInstance)  → onWebviewEvent below
//
// The `id` is what turns them into request/response. Without it, a second request landing
// before the first reply cannot be told apart from the first one answering.
var pending = {};
var seq = 0;

ft.miniProgram.onWebviewEvent(function (msg) {
  var body = (msg && msg.detail) || msg || {};
  var entry = pending[body.id];
  if (!entry) return;                 // not ours, or already settled
  delete pending[body.id];
  clearTimeout(entry.timer);
  body.ok ? entry.resolve(body.payload) : entry.reject(body.err);
});

function askShell(type, params, timeoutMs) {
  return new Promise(function (resolve, reject) {
    var id = "r" + ++seq;
    pending[id] = {
      resolve: resolve,
      reject: reject,
      // A permission prompt can sit on screen for a long time before the customer
      // answers. A short ceiling here reports a failure for a request that is still
      // perfectly alive.
      timer: setTimeout(function () {
        delete pending[id];
        reject({ errMsg: type + ": the shell did not answer in time" });
      }, timeoutMs || 120000),
    };
    ft.miniProgram.sendWebviewEvent({ id: id, type: type, params: params || {} });
  });
}

askShell("getUserInfo").then(function (profile) {
  console.log(profile.first_name, profile.mobile);
});
Be generous with the timeout. A relayed call that triggers a permission prompt does not come back until the customer answers it, and they may read it, switch apps, come back. Two minutes is reasonable; eight seconds reports a failure for a request that is still perfectly alive.
In the shell
// index.js — the shell side of the same conversation.
Page({
  data: { src: "" },

  onLoad() {
    this.setData({ src: getApp().globalData.siteUrl });
  },

  onPageEvent(e) {
    const msg = (e && e.detail) || {};
    // The runtime has been seen nesting the payload. Accept either shape rather than guess.
    const body = msg.data && typeof msg.data === "object" ? msg.data : msg;
    const { id, type, params } = body || {};
    if (!id || !type) return;                   // not a relay request; ignore quietly

    let answered = false;
    const reply = (ok, payload) => {
      // A handler that answers twice resolves one request and leaves another waiting.
      if (answered) return;
      answered = true;
      // `payload`, not `data`: the runtime may itself wrap a message in { data }, and a
      // protocol field of the same name makes the envelope and the contents impossible to
      // tell apart. That exact collision silently broke this relay once.
      ft.sendWebviewEvent({ id, ok, payload: ok ? payload : undefined,
                            err: ok ? undefined : payload }, this);
    };

    const handler = HANDLERS[type];
    if (!handler) return reply(false, { errMsg: "no relay handler for '" + type + "'" });

    // .call(HANDLERS, …) and not handler(…): a handler reaching a sibling through `this`
    // gets undefined when invoked detached, and the TypeError surfaces inside an async
    // callback long after this frame returned — so nothing replies and the page just times
    // out, which looks like a dead bridge rather than a thrown error.
    try {
      handler.call(HANDLERS, params || {}, reply);
    } catch (err) {
      reply(false, { errMsg: String((err && err.message) || err) });
    }
  },
});

Three details in there are scar tissue, and each one cost a debugging session:

The reply field is payload, not data. The runtime may itself wrap a message in { data }, so a protocol field of the same name makes the envelope and the contents impossible to tell apart. Replies get read as envelopes and silently discarded.
Guard against answering twice. A handler that replies on two paths resolves one request and leaves a later one waiting forever.
Always reply, even on a throw. If a handler throws inside an async callback, nothing replies and the page times out — which reads as a dead bridge rather than a thrown error, and sends you to the wrong side of the problem.

Controls the shell owns

A few runtime controls are driven by a real tap on a mini-app control rather than by a call — anything built on open-type. Those live on a native page in your shell, and the page navigates to it.

In practice that means one screen: the permission settings screen. The pattern is in permissions.

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