What differs when the asking happens in a web-view rather than a native page: three states that need three branches, two independent layers of gating, and one screen your page cannot open for itself.
ft.getSetting, ft.authorize and ft.getAppAuthorizeSetting are mini-app runtime APIs, so the permission flow lives in your shell and the page asks for a result through the relay.
The exceptions are the direct APIs — ft.getLocation, ft.chooseLocation, ft.chooseImage. Your page calls those itself, and the runtime raises whatever prompt is needed. For those, handle the fail rather than pre-checking anything.
This is the most common permission bug anywhere on the platform, and it is a one-character mistake: if (!state) reads undefined and false identically, and they need opposite handling.
// Three states, not two. This is the single most common permission bug, because
// `if (!state)` reads `undefined` and `false` the same way — and they need opposite
// handling.
//
// true granted → act. No prompt appears.
// undefined never asked → ft.authorize, which prompts, then act.
// false DENIED → authorize fails INSTANTLY and shows nothing. Only the
// settings screen can re-prompt.
ft.getSetting({
success: (res) => {
const state = res.authSetting["scope.userLocation"];
if (state === true) {
doTheThing();
} else if (state === false) {
// Sending them to authorize here shows nothing at all and looks like a dead button.
offerTheSettingsScreen();
} else {
ft.authorize({
scope: "scope.userLocation",
success: doTheThing,
fail: () => console.log("declined"),
});
}
},
});ft.authorize on a scope that was already denied fails instantly and shows nothing. No prompt, no dialog, no error the user can see. It looks exactly like a dead button. Once denied, only the settings screen can re-prompt."I granted it and it still failed" is almost always this. The mini-app scope and the host app's own OS permission are independent, and the prompt your user accepted moved only the first one.
// TWO layers gate these APIs, and only one of them is yours.
//
// scope — the mini-app's own permission. The prompt grants it; the mini-app settings
// screen changes it.
// app — whether Shoppingate ITSELF has the OS permission. If the host app has camera
// access denied in the phone's settings, no mini-app scope rescues it, and the
// mini-app settings screen cannot fix it.
//
// "I granted it and it still failed" is usually the second layer. Sending that user to the
// mini-app settings screen is worse than saying nothing.
ft.getSetting({
success: (scopeRes) => {
ft.getAppAuthorizeSetting({
success: (appRes) => {
const scopeGranted = scopeRes.authSetting["scope.writePhotosAlbum"] === true;
const osGranted = appRes.albumAuthorized === true;
if (scopeGranted && !osGranted) {
// Only the phone's own settings can fix this one.
console.log("Shoppingate itself lacks album access");
}
},
});
},
});The complete set of fields that second layer reports:
// What ft.getAppAuthorizeSetting reports. Check these before a call that needs one; for
// anything else, handle the call's own `fail`.
{
cameraAuthorized: true,
locationAuthorized: true,
microphoneAuthorized: true,
albumAuthorized: false,
notificationAuthorized: true,
}Check the field that matches the permission you are about to use. For a call with no field above, handle its own fail and report what it said.
A bare "failed" covers at least four different causes with four different answers, and guessing between them sends users to a screen that cannot help:
ft.* API is not in this runtimefalsetrue but Shoppingate itself lacks the OS permissionSo when a permissioned call fails, read all of it before you answer: whether the function exists, the scope state, the OS field and its value, and the host's own errMsg verbatim. One request then tells you which layer is responsible instead of leaving you to guess.
ft.openSetting() called from JavaScript does not open anything. The runtime requires that screen to be opened by a real tap on a mini-app control, and a tap inside a web-view is not one.
This is the one case the relay cannot solve, because the relay carries data and the runtime wants a gesture. The answer is a one-page detour in your shell: the page asks the shell to navigate there, and that page owns the button.
<!--
settings.fxml — a native page for exactly one reason.
ft.openSetting() called from JavaScript does not open anything. The runtime requires the
settings screen to be opened by a real tap on a mini-app control, and a tap inside a
web-view is not one. So the page cannot do this for itself: it asks the shell to navigate
here, and this page owns the button.
-->
<view class="wrap">
<view class="title">Profile access is off</view>
<view class="body">
This app reads your name and mobile from your Shoppingate account. You turned that off,
and only you can turn it back on.
</view>
<button class="btn" open-type="openSetting" bindopensetting="onOpened">Open settings</button>
</view>Send the user there only when the scope is actually false. On undefined the prompt still works, and a settings trip instead of a prompt is a worse first experience. On a denied OS permission, that screen cannot help at all — say so rather than sending them.
ft.getUserInfo prompts on every call. It is not satisfied by a stored grant the way location is, so a page that reads it per screen asks repeatedly and the user assumes something is broken.
Read it once at launch and serve a cache. Two things then matter: do not run ft.authorize in front of it — that adds a second prompt rather than removing the first — and re-check the real state on App.onShow, because the user can change their mind in Settings while your app is in the background. A cache that cannot be invalidated is worse than no cache, because it is confidently wrong in both directions.
The desc strings in app.json are rendered by the host, in its own prompt, before any of your JavaScript runs. There is no language to switch on at that point — so write both languages into the one string, Arabic first. It is the only user-facing text in your app you cannot localise at runtime.