| | |
| | | errorType: BluetoothErrorType; |
| | | error: any; |
| | | }; |
| | | type BluetoothPermissionLayerState = 'authorized' | 'denied' | 'not determined' | 'unknown'; |
| | | type BluetoothPermissionSnapshot = { |
| | | appBluetoothAuthorized: BluetoothPermissionLayerState; |
| | | miniappBluetoothScope: MiniappBluetoothAuthState; |
| | | systemInfo: Record<string, unknown>; |
| | | }; |
| | | |
| | | const BLUETOOTH_AUTH_KEY = 'ROBOTAXI_BLUETOOTH_AUTHORIZED'; |
| | | const BLUETOOTH_SCOPE = 'scope.bluetooth' as any; |
| | |
| | | if (bluetoothAuthorized === 'authorized') { |
| | | return 'authorized'; |
| | | } |
| | | if (bluetoothAuthorized === 'denied' || bluetoothAuthorized === 'not determined') { |
| | | if (bluetoothAuthorized === 'denied') { |
| | | return 'denied'; |
| | | } |
| | | } |
| | |
| | | return; |
| | | } |
| | | console.log(BLUETOOTH_ACCESS_LOG_PREFIX, message); |
| | | } |
| | | |
| | | function getBluetoothPermissionDeniedMessage(snapshot?: Partial<BluetoothPermissionSnapshot>) { |
| | | if (snapshot?.appBluetoothAuthorized === 'denied') { |
| | | return '请先在微信设置中授权蓝牙'; |
| | | } |
| | | return '请先授权蓝牙权限'; |
| | | } |
| | | |
| | | /** |
| | |
| | | const startBluetoothStateWatchRef = useRef<() => void>(() => {}); |
| | | const stopBluetoothStateWatchRef = useRef<() => void>(() => {}); |
| | | |
| | | /** |
| | | * 读取当前小程序蓝牙授权状态。 |
| | | */ |
| | | const getMiniappBluetoothAuthState = useCallback(async () => { |
| | | const getBluetoothSystemInfoSnapshot = useCallback(() => { |
| | | try { |
| | | const systemInfo = Taro.getSystemInfoSync() as any; |
| | | return { |
| | | platform: systemInfo?.platform, |
| | | brand: systemInfo?.brand, |
| | | model: systemInfo?.model, |
| | | system: systemInfo?.system, |
| | | version: systemInfo?.version, |
| | | SDKVersion: systemInfo?.SDKVersion, |
| | | bluetoothEnabled: systemInfo?.bluetoothEnabled, |
| | | locationEnabled: systemInfo?.locationEnabled, |
| | | locationAuthorized: systemInfo?.locationAuthorized |
| | | }; |
| | | } catch (error: any) { |
| | | return { |
| | | systemInfoError: error?.message || String(error || '') |
| | | }; |
| | | } |
| | | }, []); |
| | | |
| | | const readBluetoothPermissionSnapshot = useCallback(async (): Promise<BluetoothPermissionSnapshot> => { |
| | | let appBluetoothAuthorized: BluetoothPermissionLayerState = 'unknown'; |
| | | let miniappBluetoothScope: MiniappBluetoothAuthState = 'unknown'; |
| | | const systemInfo = getBluetoothSystemInfoSnapshot(); |
| | | |
| | | try { |
| | | const appAuthorizeSetting = await Promise.resolve((Taro as any).getAppAuthorizeSetting?.()); |
| | | const bluetoothAuthorized = appAuthorizeSetting?.bluetoothAuthorized; |
| | | logBluetoothAccess('读取小程序权限设置', { |
| | | bluetoothAuthorized, |
| | | appAuthorizeSetting |
| | | }); |
| | | if (typeof bluetoothAuthorized === 'string') { |
| | | if (bluetoothAuthorized === 'authorized') { |
| | | return 'authorized'; |
| | | } |
| | | if (bluetoothAuthorized === 'denied' || bluetoothAuthorized === 'not determined') { |
| | | return 'denied'; |
| | | } |
| | | appBluetoothAuthorized = bluetoothAuthorized as BluetoothPermissionLayerState; |
| | | } |
| | | logBluetoothAccess('读取微信 App 蓝牙授权状态', { |
| | | bluetoothAuthorized, |
| | | appAuthorizeSetting, |
| | | systemInfo |
| | | }); |
| | | } catch (error) { |
| | | logBluetoothAccess('读取 App 级蓝牙权限设置失败', { |
| | | error |
| | | logBluetoothAccess('读取微信 App 蓝牙授权状态失败', { |
| | | error, |
| | | systemInfo |
| | | }); |
| | | } |
| | | |
| | | try { |
| | | const setting = await Taro.getSetting(); |
| | | const authSetting = setting?.authSetting || {}; |
| | | logBluetoothAccess('回退读取小程序权限设置', { |
| | | bluetoothScope: authSetting?.[BLUETOOTH_SCOPE], |
| | | authSetting |
| | | }); |
| | | if (Object.prototype.hasOwnProperty.call(authSetting, BLUETOOTH_SCOPE)) { |
| | | return authSetting[BLUETOOTH_SCOPE] === true ? 'authorized' : 'denied'; |
| | | miniappBluetoothScope = authSetting[BLUETOOTH_SCOPE] === true ? 'authorized' : 'denied'; |
| | | } |
| | | logBluetoothAccess('读取小程序蓝牙 scope 状态', { |
| | | bluetoothScope: authSetting?.[BLUETOOTH_SCOPE], |
| | | authSetting, |
| | | systemInfo |
| | | }); |
| | | } catch (error) { |
| | | logBluetoothAccess('回退读取小程序权限设置失败', { |
| | | error |
| | | logBluetoothAccess('读取小程序蓝牙 scope 状态失败', { |
| | | error, |
| | | systemInfo |
| | | }); |
| | | } |
| | | |
| | | logBluetoothAccess('小程序蓝牙权限状态未知'); |
| | | return { |
| | | appBluetoothAuthorized, |
| | | miniappBluetoothScope, |
| | | systemInfo |
| | | }; |
| | | }, [getBluetoothSystemInfoSnapshot]); |
| | | |
| | | /** |
| | | * 读取当前小程序蓝牙授权状态。 |
| | | */ |
| | | const getMiniappBluetoothAuthState = useCallback(async () => { |
| | | const snapshot = await readBluetoothPermissionSnapshot(); |
| | | logBluetoothAccess('聚合蓝牙权限快照', snapshot); |
| | | // 优先判断拒绝态:微信 App 没权限 或 小程序没授权,都视为拒绝 |
| | | if (snapshot.appBluetoothAuthorized === 'denied' || snapshot.miniappBluetoothScope === 'denied') { |
| | | return 'denied'; |
| | | } |
| | | // 其次判断授权态:必须小程序 scope 已授权(App 层即使是 unknown 只要没拒绝就放行) |
| | | if (snapshot.miniappBluetoothScope === 'authorized') { |
| | | return 'authorized'; |
| | | } |
| | | logBluetoothAccess('蓝牙权限状态未知,等待用户触发授权或系统回传', snapshot); |
| | | return 'unknown'; |
| | | }, []); |
| | | }, [readBluetoothPermissionSnapshot]); |
| | | |
| | | const openBluetoothPermissionSetting = useCallback(async (scene: string, snapshot?: Partial<BluetoothPermissionSnapshot>) => { |
| | | const nextSnapshot = snapshot || await readBluetoothPermissionSnapshot(); |
| | | const openAppAuthorizeSetting = (Taro as any).openAppAuthorizeSetting; |
| | | const shouldOpenWechatAppSetting = nextSnapshot.appBluetoothAuthorized === 'denied' && typeof openAppAuthorizeSetting === 'function'; |
| | | logBluetoothAccess('准备打开蓝牙相关设置页', { |
| | | scene, |
| | | shouldOpenWechatAppSetting, |
| | | ...nextSnapshot |
| | | }); |
| | | if (shouldOpenWechatAppSetting) { |
| | | await openAppAuthorizeSetting(); |
| | | return 'wechat_app_setting'; |
| | | } |
| | | await Taro.openSetting(); |
| | | return 'miniapp_setting'; |
| | | }, [readBluetoothPermissionSnapshot]); |
| | | |
| | | /** |
| | | * 已确认拿到小程序蓝牙权限后,统一收口后续初始化与兜底提示。 |
| | |
| | | const refreshBluetoothStateImpl = async () => { |
| | | let hasMiniappAuth = Boolean(Taro.getStorageSync(BLUETOOTH_AUTH_KEY)); |
| | | const isAuthorizeProtectionActive = (Date.now() - recentAuthorizedAtRef.current) < BLUETOOTH_AUTH_PROTECTION_WINDOW_MS; |
| | | const systemInfoSnapshot = getBluetoothSystemInfoSnapshot(); |
| | | logBluetoothAccess('开始刷新蓝牙状态', { |
| | | cachedMiniappAuth: hasMiniappAuth, |
| | | tripStatus, |
| | | isAuthorizeProtectionActive |
| | | isAuthorizeProtectionActive, |
| | | bluetoothAuthSuccess, |
| | | systemInfo: systemInfoSnapshot |
| | | }); |
| | | try { |
| | | const appAuthorizeSetting = await Promise.resolve((Taro as any).getAppAuthorizeSetting?.()); |
| | | const bluetoothAuthorized = appAuthorizeSetting?.bluetoothAuthorized; |
| | | logBluetoothAccess('刷新蓝牙状态时读取设置', { |
| | | bluetoothAuthorized, |
| | | appAuthorizeSetting |
| | | appAuthorizeSetting, |
| | | systemInfo: systemInfoSnapshot |
| | | }); |
| | | if (bluetoothAuthorized === 'authorized') { |
| | | hasMiniappAuth = true; |
| | |
| | | } |
| | | } catch (error) { |
| | | logBluetoothAccess('刷新蓝牙状态时读取 App 级权限失败', { |
| | | error |
| | | error, |
| | | systemInfo: systemInfoSnapshot |
| | | }); |
| | | } |
| | | |
| | |
| | | const syncMiniappAuthAfterSystemPrompt = async (scene: string) => { |
| | | const latestMiniappAuthState = await getMiniappBluetoothAuthState(); |
| | | logBluetoothAccess(`${scene}后再次确认小程序蓝牙权限`, { |
| | | latestMiniappAuthState |
| | | latestMiniappAuthState, |
| | | systemInfo: systemInfoSnapshot |
| | | }); |
| | | if (latestMiniappAuthState === 'authorized') { |
| | | hasMiniappAuth = true; |
| | |
| | | |
| | | let systemBluetoothEnabled = false; |
| | | const openResult = await openBluetoothAdapterSafe(); |
| | | logBluetoothAccess('openBluetoothAdapter 结果补充诊断', { |
| | | opened: openResult.opened, |
| | | errorType: openResult.errorType, |
| | | errCode: openResult.error?.errCode, |
| | | errMsg: openResult.error?.errMsg, |
| | | systemInfo: systemInfoSnapshot |
| | | }); |
| | | if (openResult.opened) { |
| | | systemBluetoothEnabled = true; |
| | | if (!hasMiniappAuth) { |
| | |
| | | const authGrantedAfterPrompt = await syncMiniappAuthAfterSystemPrompt('蓝牙权限弹窗处理'); |
| | | if (!authGrantedAfterPrompt && !isAuthorizeProtectionActive) { |
| | | hasMiniappAuth = false; |
| | | logBluetoothAccess('openBluetoothAdapter 返回 permission_deny,当前按未授权态处理', { |
| | | appPermissionLikelyDenied: true, |
| | | systemInfo: systemInfoSnapshot, |
| | | errCode: openResult.error?.errCode, |
| | | errMsg: openResult.error?.errMsg |
| | | }); |
| | | } else if (!authGrantedAfterPrompt) { |
| | | logBluetoothAccess('授权保护窗口内忽略瞬时 permission_deny', { |
| | | errCode: openResult.error?.errCode, |
| | | errMsg: openResult.error?.errMsg |
| | | errMsg: openResult.error?.errMsg, |
| | | systemInfo: systemInfoSnapshot |
| | | }); |
| | | } |
| | | } else { |
| | |
| | | opened: openResult.opened, |
| | | errorType: openResult.errorType, |
| | | errCode: openResult.error?.errCode, |
| | | errMsg: openResult.error?.errMsg |
| | | errMsg: openResult.error?.errMsg, |
| | | systemInfo: systemInfoSnapshot |
| | | }); |
| | | |
| | | if (!systemBluetoothEnabled) { |
| | |
| | | |
| | | // 跳转系统蓝牙设置,并在返回后触发一次状态刷新 |
| | | const handleOpenSystemBluetoothSetting = useCallback(async () => { |
| | | logBluetoothAccess('用户点击去开启系统蓝牙'); |
| | | const systemInfoSnapshot = getBluetoothSystemInfoSnapshot(); |
| | | logBluetoothAccess('用户点击去开启系统蓝牙', { |
| | | systemInfo: systemInfoSnapshot |
| | | }); |
| | | try { |
| | | const openSystemBluetoothSetting = (Taro as any).openSystemBluetoothSetting; |
| | | if (typeof openSystemBluetoothSetting === 'function') { |
| | |
| | | } |
| | | } catch (error) { |
| | | logBluetoothAccess('打开系统蓝牙设置失败', { |
| | | error |
| | | error, |
| | | systemInfo: systemInfoSnapshot |
| | | }); |
| | | Taro.showToast({ title: '请手动开启系统蓝牙', icon: 'none' }); |
| | | } finally { |
| | | logBluetoothAccess('系统蓝牙设置流程结束,准备刷新状态'); |
| | | logBluetoothAccess('系统蓝牙设置流程结束,准备刷新状态', { |
| | | systemInfo: systemInfoSnapshot |
| | | }); |
| | | startOpenSettingPollingRef.current(); |
| | | setTimeout(() => { |
| | | refreshBluetoothState(); |
| | | }, 300); |
| | | } |
| | | }, [refreshBluetoothState]); |
| | | }, [getBluetoothSystemInfoSnapshot, refreshBluetoothState]); |
| | | |
| | | // 打开蓝牙授权弹窗 |
| | | const handleOpenAuthBluetoothPopup = () => setShowAuthBluetoothPopup(true); |
| | |
| | | // 执行小程序蓝牙授权,成功后展示成功弹窗 |
| | | const handleAuthorizeBluetooth = useCallback(async () => { |
| | | const miniappBluetoothAuthState = await getMiniappBluetoothAuthState(); |
| | | const permissionSnapshot = await readBluetoothPermissionSnapshot(); |
| | | logBluetoothAccess('用户触发蓝牙授权', { |
| | | miniappBluetoothAuthState |
| | | miniappBluetoothAuthState, |
| | | ...permissionSnapshot |
| | | }); |
| | | if (miniappBluetoothAuthState === 'denied') { |
| | | try { |
| | | await Taro.openSetting(); |
| | | const openedSettingType = await openBluetoothPermissionSetting('bluetooth_denied', permissionSnapshot); |
| | | logBluetoothAccess('已从拒绝态打开权限设置页', { |
| | | openedSettingType |
| | | }); |
| | | } catch (openSettingError) { |
| | | console.error('打开设置失败:', openSettingError); |
| | | logBluetoothAccess('蓝牙权限已拒绝,打开设置失败', { |
| | | openSettingError |
| | | openSettingError, |
| | | ...permissionSnapshot |
| | | }); |
| | | Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' }); |
| | | Taro.showToast({ title: getBluetoothPermissionDeniedMessage(permissionSnapshot), icon: 'none' }); |
| | | return; |
| | | } |
| | | const latestAuthState = await getMiniappBluetoothAuthState(); |
| | | const latestSnapshot = await readBluetoothPermissionSnapshot(); |
| | | logBluetoothAccess('从设置页返回后重新读取蓝牙权限', { |
| | | latestAuthState |
| | | latestAuthState, |
| | | ...latestSnapshot |
| | | }); |
| | | if (latestAuthState !== 'authorized') { |
| | | Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' }); |
| | | Taro.showToast({ title: getBluetoothPermissionDeniedMessage(latestSnapshot), icon: 'none' }); |
| | | return; |
| | | } |
| | | await finishBluetoothAuthorization(); |
| | |
| | | } catch (authorizeError) { |
| | | console.log('授权蓝牙失败:', authorizeError); |
| | | logBluetoothAccess('调用 authorize(scope.bluetooth) 失败', { |
| | | authorizeError |
| | | authorizeError, |
| | | ...permissionSnapshot |
| | | }); |
| | | try { |
| | | await Taro.openSetting(); |
| | | const openedSettingType = await openBluetoothPermissionSetting('authorize_failed', permissionSnapshot); |
| | | logBluetoothAccess('authorize 失败后已打开权限设置页', { |
| | | openedSettingType |
| | | }); |
| | | } catch (openSettingError) { |
| | | console.log('打开设置失败:', openSettingError); |
| | | logBluetoothAccess('authorize 失败后打开设置失败', { |
| | | openSettingError |
| | | openSettingError, |
| | | ...permissionSnapshot |
| | | }); |
| | | Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' }); |
| | | Taro.showToast({ title: getBluetoothPermissionDeniedMessage(permissionSnapshot), icon: 'none' }); |
| | | return; |
| | | } |
| | | const latestAuthState = await getMiniappBluetoothAuthState(); |
| | | const latestSnapshot = await readBluetoothPermissionSnapshot(); |
| | | logBluetoothAccess('authorize 失败后从设置页返回,重新读取蓝牙权限', { |
| | | latestAuthState |
| | | latestAuthState, |
| | | ...latestSnapshot |
| | | }); |
| | | if (latestAuthState !== 'authorized') { |
| | | Taro.showToast({ title: '请先授权小程序蓝牙权限', icon: 'none' }); |
| | | Taro.showToast({ title: getBluetoothPermissionDeniedMessage(latestSnapshot), icon: 'none' }); |
| | | return; |
| | | } |
| | | await finishBluetoothAuthorization(); |
| | | return; |
| | | } |
| | | await finishBluetoothAuthorization(); |
| | | }, [finishBluetoothAuthorization, getMiniappBluetoothAuthState]); |
| | | }, [finishBluetoothAuthorization, getMiniappBluetoothAuthState, openBluetoothPermissionSetting, readBluetoothPermissionSnapshot]); |
| | | |
| | | // 蓝牙卡片点击分发:未开蓝牙->去开启,未授权->弹授权,已授权->弹成功态说明 |
| | | const handleBluetoothCardClick = useCallback(() => { |