Shoppingate Logo
HomePartnerSell on E-commAbout UsContact UsDevelopers
Documentation
Get started
  • Getting started
  • Environments
Mini-app development
  • Authoring
  • app.json config
  • Lifecycle
  • Routing & query
  • Host functions
  • · orderPayment
  • Permissions
  • Deep links
Backend integration
  • Authentication
  • Endpoints
  • Webhooks
Runtime

The rule that applies to every permission

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.

Value
Meaning
What to do
undefined
Never asked
ft.authorize(...) — this shows the system prompt.
true
Granted
Call the API directly
false
Denied
ft.openSetting() — authorize will never prompt again
Never write if (!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 API
  • false — show a modal, then ft.openSetting()
  • undefined — ft.authorize(...), and call the API in its success

1. scope.camera — the camera component

Gates 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),
    });
},

2. scope.writePhotosAlbum — saving to the album

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);
    }
});

3. scope.userLocation — location

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),
});

4. scope.record — microphone

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();
},

5. scope.bluetooth — Bluetooth

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();
},

6. scope.addPhoneContact — phone contacts

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),
    });
},

7. scope.addPhoneCalendar — system calendar

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),
    });
},

8. scope.userInfo — user profile

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),
    });
},

9. scope.werun — step count

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),
    });
},

10. NO scope — ft.chooseMedia, ft.chooseImage

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.

Cancelled scopes — always true

These still appear in AuthSetting but the authorization was removed. They always return true, so never gate anything on them:

  • scope.address
  • scope.invoiceTitle
  • scope.invoice

Two layers of permission

Layer
Covers
Cleared by
Mini-app scope
just your mini-app
ft.openSetting()
OS permission
the host app as a whole
the phone's Settings app

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.

Symptom index

What you see
Cause
First-time users see an empty settings screen
if (!authSetting[x]) collapses undefined with false — check each state explicitly
authorize fails silently forever after one Deny
Once denied, only openSetting can re-prompt
The camera component shows a black rectangle
Scope check was skipped, or binderror isn't wired
A remote image won't save to the album
filePath needs a local path — download first with ft.downloadFile
Calendar event lands at the wrong time
startTime is in seconds, not milliseconds
openSetting opens an empty screen for a first-time user
You called it before ever requesting that scope — request first, openSetting second
chooseMedia never opens
You gated it on scope.camera or scope.writePhotosAlbum — those don't cover it; handle fail
Bluetooth scan doesn't stop after leaving the page
Missing ft.stopBluetoothDevicesDiscovery() in onUnload
Location is on but coordinates look wrong
type: 'wgs84' returns raw GPS; 'gcj02' returns China-corrected coordinates
Everything looks granted but the API still fails
OS-level permission is denied — check ft.getAppAuthorizeSetting()

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