res.authSetting['scope.x'] has three states. Get any one of them wrong and a first-time user is either sent to an empty settings screen or ignored forever.
ft.authorize(...) — this shows the system prompt.ft.openSetting() — authorize will never prompt againif (!res.authSetting['scope.x']). undefined is falsy, so that collapses "never asked" into "denied", and a first-time user gets sent to the settings page instead of getting the prompt. On a fresh install this fails 100% of the time.Once a user taps Deny, ft.authorize returns authorize: fail auth deny instantly, forever, with no dialog. The OS shows its permission dialog once per install. The only way back is ft.openSetting().
ft.openSetting() only lists scopes you have already requested at least once. Open it before any request and the user sees "YourApp does not use any permissions" — an empty, useless screen. This is why you must never send an undefined user there.
Every function below has the same three branches:
true — call the APIfalse — show a modal, then ft.openSetting()undefined — ft.authorize(...), and call the API in its successGates the <camera> component (the live viewfinder), not the image picker.
Ask before showing the component, so the user isn't left staring at a black rectangle.
onOpenCamera() {
ft.getSetting({
success: (res) => {
const state = res.authSetting['scope.camera'];
if (state === true) {
this.setData({ showCamera: true });
} else if (state === false) {
// Denied before — authorize can never prompt again
ft.showModal({
title: 'Camera access needed',
content: 'Turn on camera access to take a photo.',
confirmText: 'Open settings',
cancelText: 'Not now',
success: (r) => { if (r.confirm) ft.openSetting(); }
});
} else {
// undefined — never asked before, safe to prompt
ft.authorize({
scope: 'scope.camera',
success: () => {
this.setData({ showCamera: true });
},
fail: (err) => console.log('authorize fail', err),
});
}
},
fail: (err) => console.log('getSetting fail', err),
});
},
// bind to the component's binderror — fires when access is denied
onCameraError(e) {
console.log('camera error', e.detail);
this.setData({ showCamera: false });
},
onTakePhoto() {
const ctx = ft.createCameraContext();
ctx.takePhoto({
quality: 'high',
success: (res) => this.setData({ photoPath: res.tempImagePath, showCamera: false }),
fail: (err) => console.log('takePhoto fail', err),
});
},Gates ft.saveImageToPhotosAlbum and ft.saveVideoToPhotosAlbum.
This is SAVING only. It has nothing to do with picking or reading photos. Do not check it before opening an image picker — see section 10.
onSaveImage(filePath) {
ft.getSetting({
success: (res) => {
const state = res.authSetting['scope.writePhotosAlbum'];
if (state === true) {
ft.saveImageToPhotosAlbum({
filePath: filePath, // local path, not a URL
success: () => ft.showToast({ title: 'Saved to album', icon: 'success' }),
fail: (err) => console.log('saveImageToPhotosAlbum fail', err),
});
} else if (state === false) {
ft.showModal({
title: 'Photo access needed',
content: 'Turn on photo access to save this image.',
confirmText: 'Open settings',
cancelText: 'Not now',
success: (r) => { if (r.confirm) ft.openSetting(); }
});
} else {
ft.authorize({
scope: 'scope.writePhotosAlbum',
success: () => {
ft.saveImageToPhotosAlbum({
filePath: filePath,
success: () => ft.showToast({ title: 'Saved to album', icon: 'success' }),
fail: (err) => console.log('saveImageToPhotosAlbum fail', err),
});
},
fail: (err) => console.log('authorize fail', err),
});
}
},
fail: (err) => console.log('getSetting fail', err),
});
},For video, use ft.saveVideoToPhotosAlbum instead — the scope is the same.
A remote image has to be downloaded first; filePath will not accept a URL:
ft.downloadFile({
url: 'https://example.com/photo.jpg',
success: (res) => {
if (res.statusCode === 200) this.onSaveImage(res.tempFilePath);
}
});Gates ft.getLocation and ft.chooseLocation.
onGetLocation() {
ft.getSetting({
success: (res) => {
const state = res.authSetting['scope.userLocation'];
if (state === true) {
ft.getLocation({
type: 'gcj02', // 'wgs84' returns raw GPS coordinates
success: (r) => console.log('lat', r.latitude, 'lng', r.longitude),
fail: (err) => console.log('getLocation fail', err),
});
} else if (state === false) {
ft.showModal({
title: 'Location access needed',
content: 'Turn on location access to find nearby branches.',
confirmText: 'Open settings',
cancelText: 'Not now',
success: (r) => { if (r.confirm) ft.openSetting(); }
});
} else {
ft.authorize({
scope: 'scope.userLocation',
success: () => {
ft.getLocation({
type: 'gcj02',
success: (r) => console.log('lat', r.latitude, 'lng', r.longitude),
fail: (err) => console.log('getLocation fail', err),
});
},
fail: (err) => console.log('authorize fail', err),
});
}
},
fail: (err) => console.log('getSetting fail', err),
});
},ft.chooseLocation uses the same scope.userLocation — same function, swap the API:
ft.chooseLocation({
success: (r) => console.log(r.name, r.address, r.latitude, r.longitude),
fail: (err) => console.log('chooseLocation fail', err),
});Gates ft.startRecord.
onStartRecording() {
ft.getSetting({
success: (res) => {
const state = res.authSetting['scope.record'];
if (state === true) {
ft.startRecord({
success: (r) => console.log('recording saved to', r.tempFilePath),
fail: (err) => console.log('startRecord fail', err),
});
} else if (state === false) {
ft.showModal({
title: 'Microphone access needed',
content: 'Turn on microphone access to record.',
confirmText: 'Open settings',
cancelText: 'Not now',
success: (r) => { if (r.confirm) ft.openSetting(); }
});
} else {
ft.authorize({
scope: 'scope.record',
success: () => {
ft.startRecord({
success: (r) => console.log('recording saved to', r.tempFilePath),
fail: (err) => console.log('startRecord fail', err),
});
},
fail: (err) => console.log('authorize fail', err),
});
}
},
fail: (err) => console.log('getSetting fail', err),
});
},
onStopRecording() {
ft.stopRecord();
},Gates ft.openBluetoothAdapter and ft.createBLEPeripheralServer.
onConnectDevice() {
ft.getSetting({
success: (res) => {
const state = res.authSetting['scope.bluetooth'];
if (state === true) {
ft.openBluetoothAdapter({
success: () => ft.startBluetoothDevicesDiscovery({
success: () => console.log('scanning'),
}),
fail: (err) => console.log('openBluetoothAdapter fail', err),
});
} else if (state === false) {
ft.showModal({
title: 'Bluetooth access needed',
content: 'Turn on Bluetooth access to connect a device.',
confirmText: 'Open settings',
cancelText: 'Not now',
success: (r) => { if (r.confirm) ft.openSetting(); }
});
} else {
ft.authorize({
scope: 'scope.bluetooth',
success: () => {
ft.openBluetoothAdapter({
success: () => ft.startBluetoothDevicesDiscovery({
success: () => console.log('scanning'),
}),
fail: (err) => console.log('openBluetoothAdapter fail', err),
});
},
fail: (err) => console.log('authorize fail', err),
});
}
},
fail: (err) => console.log('getSetting fail', err),
});
},
onUnload() {
ft.stopBluetoothDevicesDiscovery();
ft.closeBluetoothAdapter();
},Gates ft.addPhoneContact. firstName is required; every other field is optional.
onSaveContact() {
ft.getSetting({
success: (res) => {
const state = res.authSetting['scope.addPhoneContact'];
if (state === true) {
ft.addPhoneContact({
firstName: 'Luxe',
lastName: 'Spa',
mobilePhoneNumber: '+966500000000',
success: () => ft.showToast({ title: 'Contact saved', icon: 'success' }),
fail: (err) => console.log('addPhoneContact fail', err),
});
} else if (state === false) {
ft.showModal({
title: 'Contacts access needed',
content: 'Turn on contacts access to save this number.',
confirmText: 'Open settings',
cancelText: 'Not now',
success: (r) => { if (r.confirm) ft.openSetting(); }
});
} else {
ft.authorize({
scope: 'scope.addPhoneContact',
success: () => {
ft.addPhoneContact({
firstName: 'Luxe',
lastName: 'Spa',
mobilePhoneNumber: '+966500000000',
success: () => ft.showToast({ title: 'Contact saved', icon: 'success' }),
fail: (err) => console.log('addPhoneContact fail', err),
});
},
fail: (err) => console.log('authorize fail', err),
});
}
},
fail: (err) => console.log('getSetting fail', err),
});
},Gates ft.addPhoneCalendar and ft.addPhoneRepeatCalendar.
startTime is a Unix timestamp in seconds, not milliseconds.
onAddToCalendar(booking) {
ft.getSetting({
success: (res) => {
const state = res.authSetting['scope.addPhoneCalendar'];
if (state === true) {
ft.addPhoneCalendar({
title: 'Spa appointment',
startTime: Math.floor(new Date(booking.date).getTime() / 1000),
allDay: false,
description: booking.serviceName,
location: booking.branch,
alarm: true,
alarmOffset: 3600, // remind 1 hour before
success: () => ft.showToast({ title: 'Added to calendar', icon: 'success' }),
fail: (err) => console.log('addPhoneCalendar fail', err),
});
} else if (state === false) {
ft.showModal({
title: 'Calendar access needed',
content: 'Turn on calendar access to add this booking.',
confirmText: 'Open settings',
cancelText: 'Not now',
success: (r) => { if (r.confirm) ft.openSetting(); }
});
} else {
ft.authorize({
scope: 'scope.addPhoneCalendar',
success: () => {
ft.addPhoneCalendar({
title: 'Spa appointment',
startTime: Math.floor(new Date(booking.date).getTime() / 1000),
allDay: false,
description: booking.serviceName,
location: booking.branch,
alarm: true,
alarmOffset: 3600,
success: () => ft.showToast({ title: 'Added to calendar', icon: 'success' }),
fail: (err) => console.log('addPhoneCalendar fail', err),
});
},
fail: (err) => console.log('authorize fail', err),
});
}
},
fail: (err) => console.log('getSetting fail', err),
});
},Gates ft.getUserInfo.
onGetUserInfo() {
ft.getSetting({
success: (res) => {
const state = res.authSetting['scope.userInfo'];
if (state === true) {
ft.getUserInfo({
success: (r) => console.log(r.userInfo.nickName, r.userInfo.avatarUrl),
fail: (err) => console.log('getUserInfo fail', err),
});
} else if (state === false) {
ft.showModal({
title: 'Profile access needed',
content: 'Turn on profile access to continue.',
confirmText: 'Open settings',
cancelText: 'Not now',
success: (r) => { if (r.confirm) ft.openSetting(); }
});
} else {
ft.authorize({
scope: 'scope.userInfo',
success: () => {
ft.getUserInfo({
success: (r) => console.log(r.userInfo.nickName, r.userInfo.avatarUrl),
fail: (err) => console.log('getUserInfo fail', err),
});
},
fail: (err) => console.log('authorize fail', err),
});
}
},
fail: (err) => console.log('getSetting fail', err),
});
},Gates ft.getWeRunData. The data comes back encrypted and is decrypted on your server.
onGetSteps() {
ft.getSetting({
success: (res) => {
const state = res.authSetting['scope.werun'];
if (state === true) {
ft.getWeRunData({
success: (r) => console.log('encrypted payload', r.encryptedData),
fail: (err) => console.log('getWeRunData fail', err),
});
} else if (state === false) {
ft.showModal({
title: 'Fitness access needed',
content: 'Turn on fitness access to read your step count.',
confirmText: 'Open settings',
cancelText: 'Not now',
success: (r) => { if (r.confirm) ft.openSetting(); }
});
} else {
ft.authorize({
scope: 'scope.werun',
success: () => {
ft.getWeRunData({
success: (r) => console.log('encrypted payload', r.encryptedData),
fail: (err) => console.log('getWeRunData fail', err),
});
},
fail: (err) => console.log('authorize fail', err),
});
}
},
fail: (err) => console.log('getSetting fail', err),
});
},These are not in the scope list at all. There is nothing to getSetting and nothing to authorize. The picker raises the OS prompt itself the first time it runs.
Do not gate them on scope.camera (that is the <camera> component) or on scope.writePhotosAlbum (that is saving). Doing so sends every user down a broken path.
Handle the failure instead:
onPickImage() {
ft.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
camera: 'back',
success: (res) => {
const path = res.tempFiles[0].tempFilePath;
this.setData({ imagePath: path });
},
fail: (err) => {
console.log('chooseMedia fail', err);
// A cancel is the user backing out — don't nag them
if ((err.errMsg || '').indexOf('cancel') !== -1) return;
// Anything else is a refused OS permission
ft.showModal({
title: 'Camera access needed',
content: 'Turn on camera access to attach a photo.',
confirmText: 'Open settings',
cancelText: 'Not now',
success: (r) => { if (r.confirm) ft.openSetting(); }
});
}
});
},ft.chooseImage works the same way but returns res.tempFilePaths (an array of strings) instead of res.tempFiles.
These still appear in AuthSetting but the authorization was removed. They always return true, so never gate anything on them:
scope.addressscope.invoiceTitlescope.invoiceft.openSetting()ft.openSetting() only manages the first. If the user denied Camera to the host app at the OS level, no mini-app API can re-prompt or deep-link around it — you can only tell them where to go. ft.getAppAuthorizeSetting() reports that outer layer:
const appAuth = ft.getAppAuthorizeSetting();
if (!appAuth.cameraAuthorized) {
// blocked on the HOST APP — openSetting cannot fix this
ft.showToast({ title: 'Enable Camera for this app in your phone Settings', icon: 'none' });
}It reports cameraAuthorized, locationAuthorized, microphoneAuthorized, albumAuthorized, notificationAuthorized.
if (!authSetting[x]) collapses undefined with false — check each state explicitlyauthorize fails silently forever after one DenyopenSetting can re-promptbinderror isn't wiredfilePath needs a local path — download first with ft.downloadFilestartTime is in seconds, not millisecondsopenSetting opens an empty screen for a first-time useropenSetting secondchooseMedia never opensscope.camera or scope.writePhotosAlbum — those don't cover it; handle failft.stopBluetoothDevicesDiscovery() in onUnloadtype: 'wgs84' returns raw GPS; 'gcj02' returns China-corrected coordinatesft.getAppAuthorizeSetting()