The shell mini-app that hosts your page: six files, and only four values in them that you need to change. Studio's Webview template generates it; this page is what it generates and why each piece is there.
miniapp/ app.js siteUrl, the mini-app id, any launch-time caching app.json two pages, the window, the scopes you ask for SGConf.js EMPTY — a web-view wrap declares no extension APIs permissions.js one table the shell dispatches from and the page renders pages/index/ the <web-view> and the relay. The whole mechanism. pages/settings/ the native button that opens the permission settings screen
Two pages, not one. The second exists only because of a platform rule covered in permissions: ft.openSetting() called from JavaScript does nothing, so re-opening the permission screen needs a real tap on a native control.
<web-view> tagOne tag fills the page. The attributes you bind on it are the entire inbound half of the bridge, and choosing the wrong event is the most common early mistake.
<!--
bindevent fires as soon as the page sends. Use this for anything live.
bindmessage fires only in a batch when the page is LEFT — not when it is sent.
bindload / binderror are the only signal that the URL loaded at all.
-->
<web-view
src="{{src}}"
bindevent="onPageEvent"
bindmessage="onPageMessage"
bindload="onPageLoad"
binderror="onPageError">
</web-view>bindeventbindmessagebindloadbinderrorbindtimeoutft.complete() in time.bindmessage sounds like the one you want and is almost never it. It delivers nothing until the user leaves the page, so a request sent through it appears to vanish. Use bindevent for anything you expect an answer to.App({
globalData: {
// Where your page is served from.
//
// localhost — simulator only, and needs urlCheck off. A device cannot reach your
// laptop, and localhost can never be allow-listed.
// https — what a device needs, and the host must be registered in the
// developer portal or the web-view loads nothing.
siteUrl: "https://your-page.example.com/",
// Sent as `miniappId` on host-function calls.
miniAppId: "",
},
onLaunch() {
console.log("[shell] wrapping", this.globalData.siteUrl);
},
});Declare only the scopes you actually use. Asking for Bluetooth and the address book in an app that needs neither is a review risk and an obvious trust problem for the user who reads the prompt.
{
"pages": [
"pages/index/index",
"pages/settings/settings"
],
"window": {
"navigationBarTitleText": "Your app",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundTextStyle": "light",
"backgroundColor": "#f5f5f7"
},
"permission": {
"scope.userLocation": {
"desc": "لتحديد موقعك وإظهار الخيارات القريبة\n\nUsed to find your location and show nearby options."
}
},
"style": "v2",
"sitemapLocation": "sitemap.json"
}Each desc is rendered by the host, in its own prompt, before any of your JavaScript runs. There is no language to switch on at that moment — so put both languages in the one string, Arabic first. This is the one place in your app where you cannot localise at runtime.
// EMPTY ON PURPOSE. A web-view wrap needs no entries here.
//
// Declaring a name is what creates `ft.<name>` on the mini-app runtime, so an entry is
// owed only when a NATIVE page calls `ft.<name>(…)`. A web page calling
// ft.callNativeAPI('<name>', …) reaches the host directly and never consults this table.
module.exports = {
extApi: [],
};Declaring a name in extApi is what creates ft.<name> on the mini-app runtime. That matters for a native page calling ft.orderPayment(…). It does not matter for your page calling ft.callNativeAPI('orderPayment', …), which reaches the host directly and never consults this table.
extApi is not evidence that a host function is unavailable to your page. You only owe an entry when you add a native page of your own that calls one.If you do add one: params is a type contract, not an example. The container checks the shape of what you pass against those placeholders before dispatching, and rejects a mismatch without ever reaching an implementation — a rejection that is indistinguishable from api not implemented.
Four values, and then it hosts anything:
app.js → globalData.siteUrlapp.js → globalData.miniAppId, and fide.project.config.json → appidapp.json → window.navigationBarTitleTextapp.json → permission — see belowWhat a page can reach — and how the shell relays what it cannot.